Добрый день. Задача: подгрузить в процесс DLLку, которую для этого (да и вообще) нельзя сохранять на диск, и ещё и так, чтобы её настоящее имя не светилось в списке подгруженных модулей (ненастоящее можно). Читал тут это, это, это и это, много думал. Решил обойтись без KnownDlls, т.к. на это могут ругаться всякие антивирусы, да и внедрение должно происходить только в один процесс. Делаю так (эксперимент с целью, так сказать, proof of concept): 1. Гружу DLLку штатно через LoadLibrary() 2. Делаю GetModuleInformation(), узнаю по какому адресу она висит и сколько места занимает. Запоминаю это всё в буфер. 3. Выгружаю DLLку через FreeLibrary() 4. Создаю секцию Естественно, параметры тут не такие, как в LdrpFindOrMapDll(), потому что там делается memory mapping, с FILE_WRITE_EA и SEC_IMAGE. Но я ж так не могу, файла не должно быть. 5. Делаю 6. Пишу в получившийся view ранее сохранённый образ DLLки. 7. Делаю ему NtUnmapViewOfSection() 8. Ставлю хуки на NtOpenFile() и NtCreateSection() 9. Вызываю LoadLibrary() с именем какой-нибудь безобидной системной DLLки. 10. Когда внутри LdrpFindOrMapDll() дело доходит до NtOpenFile(), запоминаю хэндл файла. 11. Когда внутри LdrpFindOrMapDll() дело доходит до NtCreateSection() и запомненным выше хэндлом, вместо вызова родной функции возвращаю тот хэндл секции из п.4. По моему чайниковскому разумению, после этого должен типа как "загрузиться" модуль с тем образом, который я предварительно записал в свою секцию. Однако, несмотря на то, что последующий за NtCreateSection() вызов NtMapViewOfSection() внутри LdrpFindOrMapDll() возвращает STATUS_SUCCESS, DLLка нифига не грузится и её точка входа не выполняется. Что же делать?
Секция открывается по имени из директории KnownDlls. Директория открывается при инициализации процесса и далее описатель её хранится в системной переменной(LdrpKnownDllObjectDirectory). При открытии секции он передаётся в сервис. Можно создать свою директорию и загрузить её в переменную. Проты детектят манипуляции с этой директорией, так как возможна перезапись секции(удаление и повторное создание). Мне это тоже не нравится. Ну во первых очевидно что таким образом модуль не будет релоцироваться. STATUS_IMAGE_NOT_AT_BASE возвращается только для SEC_IMAGE. Далее в 11 нужно убрать флаг SEC_IMAGE. Есчо там проверка ZwQuerySection(SectionImageInformation), такая вроде при запуске процесса не даёт его из длл запустить. 1. Включить логгер. 2. Изменить способ загрузки. Никаких патчей недолжно быть.
Хорошо. Делаю через KnownDlls, всё равно не получается. Делал так, как написано в этой теме, перевёл код на FPC дословно (для проверки работоспособности): Код (Text): procedure CreateKnownDllSection(const SectionHandle : PHANDLE; const SectionName : PSTR; const SectionSize : ULONG; const SourceAddress : PVOID; const SourceSize : ULONG); stdcall; const DirectoryName : AnsiString = '\KnownDlls'#$00; var ObjAttr : OBJECT_ATTRIBUTES; LocalSectionHandle : HANDLE; DirectoryNameUni : UNICODE_STRING; SectionNameUni : UNICODE_STRING; LocalSectionSize : LARGE_INTEGER; ViewSize : ULONG; BaseAddress : PVOID; SectionOffset : LARGE_INTEGER; begin Assert(RtlCreateUnicodeStringFromAsciiz(@DirectoryNameUni,PAnsiChar(DirectoryName)),'RtlCreateUnicodeStringFromAsciiz 0'); ObjAttr.Length:=SizeOf(OBJECT_ATTRIBUTES); ObjAttr.RootDirectory:=0; ObjAttr.ObjectName:=@DirectoryNameUni; ObjAttr.Attributes:=0; ObjAttr.SecurityDescriptor:=nil; ObjAttr.SecurityQualityOfService:=nil; LocalSectionSize.HighPart:=0; LocalSectionSize.LowPart:=SectionSize; SectionHandle^:=0; if (SourceSize<=SectionSize) then begin Assert(NtOpenDirectoryObject(@ObjAttr.RootDirectory,DIRECTORY_CREATE_OBJECT,@ObjAttr)=STATUS_SUCCESS,'NtOpenDirectoryObject'); Assert(RtlCreateUnicodeStringFromAsciiz(@SectionNameUni,PAnsiChar(SectionName)),'RtlCreateUnicodeStringFromAsciiz 1'); ObjAttr.ObjectName:=@SectionNameUni; Assert(NtCreateSection(@LocalSectionHandle,SECTION_ALL_ACCESS,@ObjAttr,@LocalSectionSize,PAGE_EXECUTE_READWRITE,SEC_COMMIT,0)=STATUS_SUCCESS,'NtCreateSection'); BaseAddress:=nil; ViewSize:=0; SectionOffset.LowPart:=0; SectionOffset.HighPart:=0; Assert(NtMapViewOfSection(LocalSectionHandle,NtCurrentProcess,@BaseAddress,0,0,@SectionOffset,@ViewSize,ViewShare,0,PAGE_READWRITE)=STATUS_SUCCESS,'NtMapViewOfSection'); Assert(NtWriteVirtualMemory(NtCurrentProcess,BaseAddress,SourceAddress,SourceSize,nil)=STATUS_SUCCESS,'NtWriteVirtualMemory'); Assert(NtUnmapViewOfSection(NtCurrentProcess,BaseAddress)=STATUS_SUCCESS,'NtUnmapViewOfSection'); SectionHandle^:=LocalSectionHandle; RtlFreeUnicodeString(@SectionNameUni); Assert(NtClose(ObjAttr.RootDirectory)=STATUS_SUCCESS,'NtClose'); RtlFreeUnicodeString(@DirectoryNameUni); end; end; Далее так: Код (Text): CreateKnownDllSection(@hSect,MySection,DLLImage.Level,DLLImage.Ptr,DLLImage.Level); Writeln('LoadLibrary = ',LoadLibrary(PWideChar(WideString(MySection)))); Writeln('GetLastError = ',IntToString(GetLastError,Radix_HEX,0)); Ни один assert не выскочил, но LoadLibrary возвращает 0, GetLastError тоже 0, а в логгере так: STATUS_DLL_NOT_FOUND, то есть секцию загрузчик не нашёл. Как быть?
Да я бы с радостью, а что именно отлаживать, подскажите? LoadLibrary доходит до LdrpFindKnownDll, в нём вызывается NtOpenSection с именем DLL и возвращает STATUS_OBJECT_NAME_NOT_FOUND. Значит, как-то не так создаётся секция.
Так. Проблема проясняется. Надо было вместо KnownDlls писать KnownDlls32, т.к. всё это хозяйство у меня крутится на wow64. Теперь похоже на то что DLL грузится нормально, но вылазит такой косяк: Т.е. пытается обратиться по старому адресу, когда она ещё загружена была обычным способом. Т.е. надо делать релок (вручную?). Как это делать, я пока не знаю Нашёл тут похожую задачу, буду копать.
В идеале нужно релоцировать загрузчик, получить управление в начале его работы и выполнить маршрутизацию на релоцированную копию, в которой стабы должным образом скорректированы. Тут старый лодер на трейсе: indy-vx.narod.ru/Bin/Ldr.zip и тут есчо чтото связанное с загрузкой: indy-vx.narod.ru/Bin/LdrExts.zip indy-vx.narod.ru/Bin/LdrUpd.zip
Так... Мучения мои продолжаются Релок я сделал, таким образом Код (Text): function RelocateDLLImage(const DLLImage : Pointer; const DLLOriginalBase : PtrUInt; const DLLNewBase : PtrUInt) : LongBool; stdcall; var NTHeader : PIMAGE_NT_HEADERS; RelEntry : PIMAGE_BASE_RELOCATION; PatchCount : DWORD; RelPtrOffset : PtrUInt; begin Result:=False; if (PIMAGE_DOS_HEADER(DLLImage)^.e_magic=IMAGE_DOS_SIGNATURE) then // Check if this is true dll with MZ begin NTHeader:=MemOffsetPointer(DLLImage,PIMAGE_DOS_HEADER(DLLImage)^.e_lfanew); if (NTHeader^.Signature=IMAGE_NT_SIGNATURE) then // Check if this is PE dll begin // process relocation information RelPtrOffset:=NTHeader^.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress; RelEntry:=MemOffsetPointer(DLLImage,RelPtrOffset); while (RelEntry<>nil) do if (RelEntry^.SizeOfBlock>0) then begin PatchCount:=((RelEntry^.SizeOfBlock-SizeOf(IMAGE_BASE_RELOCATION)) div SizeOf(Word)); RelEntry:=LdrProcessRelocationBlock(MemOffsetPointer(DLLImage,RelEntry^.VirtualAddress), PatchCount, MemOffsetPointer(RelEntry,SizeOf(IMAGE_BASE_RELOCATION)), PtrInt(DLLNewBase-DLLOriginalBase)); end else RelEntry:=nil; // Fix ImageBase, is it necessary? NTHeader^.OptionalHeader.ImageBase:=DLLNewBase; // done Result:=True; end; // PE signature end; // MZ signature end; По крайней мере, теперь когда происходит Вызывается она по правильному адресу и даже успевает что-то там отработать. Но потом всё равно падает по AV при доступе по старому адресу Код (Text): Access violation when reading [10005000] - Shift+Run/Step to pass exception to the program Буду выяснять, откуда берётся этот AV. Может есть какие-то идеи?
Теперь оно падает из-за кривого FPC RTL. Там поддержка threadvar реализуется какой-то магией, которая только один раз при старте запоминает адреса магических функций. Переписал тестовую DLL на асме -- всё работает без ошибок
На всякий случай, если кто-нибудь будет страдать той же хернёй, что и я тут. Способ через подстановку kernel section в KnownDlls без файла на диске, т.е. чисто в памяти -- не работает. Вернее, работает через раз, случайным образом. Причина заключается, как мне подсказал kejcerfcrv выше, в этом: Если подробнее, то вызов NtCreateSection() без флага SEC_IMAGE (т.к. мы берём образ из памяти) вынуждает последующие вызовы NtMapViewOfSection() относиться к этой секции просто как к куску данных, а не образу DLL. Удивительно, но факт -- NtMapViewOfSection() вкурсе про формат PE, и если секция была создана с SEC_IMAGE, то может самостоятельно сообщить о том, что образ отмаплен не по тому адресу, который указан в ImageBase. Более того, с этим флагом функция будет стараться мапить образ именно по указанному ImageBase и\или хотя бы по тем адресам, где он (возможно) был смаплен в этом же процессе ранее. Без SEC_IMAGE нифига этого не происходит. Даже повторный вызов NtMapViewOfSection() внутри LdrpFindOrMapDLL() на ту же самую секцию вернёт адрес view другой и не вызовет релоки. Иногда, совершенно случайно, адрес view возвращается тот же самый и всё работает как надо. Но редко. В общем, такой метод не подходит. В качестве временного рабочего решения я применил полу-ручную загрузку DLL. То есть, оставляя образ в памяти, не трогая kernel sections, я прямо в нём делаю релокации, обнуляю секции с UNINITIALIZED_DATA (это важно для инициализации ЯВУшных RTL) и расставляю VirtualProtect'ы секций. Если при этом следить, чтобы внедрение происходило в процесс, где все зависимые модули уже загружены и в середине кода DLL не трогать наш якобы hInstance в вызовах винапи, то тут уже можно вызывать entry point и всё работает. Загруженная таким образом DLL нигде не светится, естественно - побочный эффект Конечно, более кошерным будет проход по таблице импорта. Это я сделаю позднее, сейчас важно было убедиться что этот подход хотя бы в принципе работоспособен.
Incidence Нужно с умом делать, вы даже не удосужились посмотреть готовое, работающее через эту директорию. Кошерное решение это стековая маршрутизация в изменённую проекцию. Это единственно годное решение(идеальное). Всякого рода захват загрузчика и дальнейшая маршрутизация хотя и будет работать, но не соверщенна.