首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >DLL 注入

DLL 注入

作者头像
Khan安全团队
发布于 2022-01-16 20:29:04
发布于 2022-01-16 20:29:04
6K0
举报
文章被收录于专栏:Khan安全团队Khan安全团队

介绍

什么是 DLL

根据MSDN,DLL 是一个库,其中包含可以由多个程序同时使用的代码和数据。 DLL 通常用于将程序模块化为单独的组件,如果模块存在,则每个模块都由主程序加载。这些模块通常扩展主程序的功能。

什么是DLL注入

由于注入的 dll 可以操纵正在运行的进程,因此它为我们提供了向应用程序添加我们想要的任何功能的绝佳机会。这通常在游戏黑客或当您想要对某些东西进行逆向工程并想要更多控制时完成。

这篇文章的目的

这篇文章将介绍如何使用 LoadLibrary 执行基本的 dll 注入,然后深入探讨 LoadLibrary 如何在幕后工作,并完成手动映射和将 DLL 注入进程的步骤。

使用 LoadLibrary 和 CreateRemoteThread

在 Windows 中进行 DLL 注入的最基本方法是使用内置函数 LoadLibrary 和 CreateRemoteThread。这要求我们在机器上拥有 dll 并要求我们知道 dll 的路径。

加载库

像大多数 winapi 函数一样,LoadLibrary 有一个 LoadLibraryA 和一个 LoadLibraryW 函数。对于那些以前没有使用过winapi的人来说,这只是表示函数期望的字符串类型。在这篇文章中,我们将使用 LoadLibraryA 函数,这仅仅是因为个人喜好以及我通常如何设置工具。

LoadLibraryA使我们能够将 dll 从磁盘加载到内存中。这个函数为我们完成了所有的工作,只需要我们将路径传递给 dll 就可以了。如果成功,它将向我们传递加载模块的句柄,如果失败,它将返回 NULL。LoadLibrary还有一个扩展功能,可让您使用标志的第二个参数设置其他加载选项。

LoadLibrary 会将我们指定的模块(dll)加载到任何调用它的地址空间中,这就是为什么我们不能单独使用它,因为这只会将 dll 加载到我们的程序中,而不是我们想要的程序中将我们的代码注入。

创建远程线程

CreateRemoteThread是我们需要使用的另一个函数来执行我们的注入。CreateRemoteThread 让我们在我们想要注入代码的进程中执行 LoadLibrary 调用。

要使用 CreateRemoteThread,我们需要一个指向我们要注入的进程的句柄、一个指向我们要调用的函数 (LoadLibraryA) 的指针以及函数的参数 (dll 路径)。如果函数成功,我们将获得线程的句柄,否则,函数将返回 NULL。

与 LoadLibrary 一样,如果您想对线程的创建进行更多控制,也可以使用该函数的扩展版本。

查找进程的句柄

上面我提到过需要一个进程的句柄,所以现在我将深入探讨句柄是什么以及如何为你的进程找到一个句柄。

在 winapi 中,HANDLE 是一种抽象,它对用户隐藏内存地址,重新组织内存而不需要程序知道所有内容。因此,进程句柄基本上只是告诉我们该进程的内存在哪里。

为了获得进程的句柄,我们需要使用 winapi 函数OpenProcess。这个函数需要被告知我们想要什么访问权限,然后它需要处理id。对于我们的注入器,我们可以只使用PROCESS_ALL_ACCESS访问权限。

您可以通过任务管理器通过转到详细信息并查看进程旁边的 PID 来找到进程 ID,但这需要时间并且不是我们想要的方式。幸运的是,我们可以通过编程方式做到这一点:

代码语言:javascript
复制
DWORD proc::GetProcId(const wchar_t* procName)
{
    // Assign to 0 for error handling
    DWORD procId = 0;
    // Takes snapshot of the processes
    HANDLE hSnap = (CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
    // Check if snapshot exists and didn't error out
    if (hSnap != INVALID_HANDLE_VALUE) {
        PROCESSENTRY32 procEntry;
        // Set entry size
        procEntry.dwSize = sizeof(procEntry);
        // Grabs first process in the snapshot and stores in procEntry
        if (Process32First(hSnap, &procEntry)) {
            // Loops through all processes
            do
            {
                // Checks if the process name is our process name
                if (!_wcsicmp(procEntry.szExeFile, procName)) {
                    // When found it saves the id and breaks out of the loop
                    procId = procEntry.th32ProcessID;
                    break;
                }
            } while (Process32Next(hSnap, &procEntry));
        }
    }
    // Closes Handle
    CloseHandle(hSnap);
    // Returns process id
    return procId;
}

当给定进程名称时,此函数将遍历所有进程并尝试查找名称与我们的进程名称匹配的进程。然后它返回进程 ID,然后我们可以使用它通过 OpenProcess 获取进程句柄。

潜入代码

由于我们只需要上面讨论的两个函数,我们可以将执行注入的所有逻辑包装在一个函数中:

代码语言:javascript
复制
void injector::LocAInject(const char* dllPath, HANDLE hProc)
{
    // Gets address to LoadLibraryA function
	LPVOID libAAddr = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");

    // Allocate space for our dll path in the process we want to inject into
	void* loc = VirtualAllocEx(hProc, 0, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    // Patch the memory we allocated and write our dll path to it
	mem::PatchEx((BYTE *)loc, (BYTE*)dllPath, (unsigned int)(strlen(dllPath) + 1), hProc);

    // Create the remote thread with LoadLibraryA and the dll path
	HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)libAAddr, loc, 0, 0);

    // If the thread isn't null then close the handle 
	if (hThread != NULL) {
		CloseHandle(hThread);
	}
    // Otherwise exit gracefully
	else {
        ErrorHandling::ErrorExit((LPTSTR)(L"CreateRemoteThread"));
	}
}

该函数接受两个参数,我们的 dll 的路径,以及我们可以在上一节中找到的进程的句柄。

代码语言:javascript
复制
LPVOID libAAddr = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");

在第一行代码中,我们使用 winapi 函数GetProcAddress获取 LoadLibraryA 的地址。该函数接受包含函数的模块的句柄和函数的名称。LoadLibraryA 位于 kernel32.dll 中,这就是我们获取该模块的原因。

代码语言:javascript
复制
void* loc = VirtualAllocEx(hProc, 0, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

下一行代码让我们在要注入代码的进程中分配等于窗口中最大路径长度的空间。这是必要的,因为我们需要调用 LoadLibrary 的路径,并且我们需要字符串在我们调用函数的进程中。

代码语言:javascript
复制
mem::PatchEx((BYTE *)loc, (BYTE*)dllPath, (unsigned int)(strlen(dllPath) + 1), hProc);

在另一个进程中分配空间后,我们需要修补该内存并将我们的 dll 路径写入我们分配的空间。为此,我使用了CPPToolLib 中的补丁外部函数,但是,该函数相当短,可以通过多种不同方式实现,因此我不会过多介绍。

代码语言:javascript
复制
HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)libAAddr, loc, 0, 0);

在将 dll 路径写入其他进程并获得指向 LoadLibrary 函数的指针之后,我们现在可以调用 CreateRemoteThread。我将 hThread 设置为 CreateRemoteThread 的返回值,以便检查它是否成功注入。

代码语言:javascript
复制
if (hThread != NULL) {
	CloseHandle(hThread);
}
else {
    ErrorHandling::ErrorExit((LPTSTR)(L"CreateRemoteThread"));
}

最后,我们检查是否成功创建线程,如果成功则关闭句柄,否则调用错误处理函数。

手动执行 LoadLibrary

我们将深入了解如何手动执行 LoadLibrary,这篇文章将介绍 32 位,64 位有一些细微差别,但在阅读完这篇文章后,您应该能够轻松地更新基于 MSDN 的 64 位代码。手动映射 DLL 可以让您执行 LoadLibrary 将 dll 加载到另一个进程中所做的所有操作,而无需将 dll 显示在模块列表中,这意味着如果某些程序试图遍历所有加载的模块,他们将看不到您的 dll。

LoadLibrary 究竟为我们做了什么

手动映射和注入 DLL 的第一步是了解 Windows 如何在幕后执行此操作。这个过程可以分为5个步骤:

  1. 阅读和解析
    • 将文件读入内存
    • 获取标题
  2. 分配内存
    • 获取和更新图像大小
    • 将标题复制到内存中
    • 用新的基础更新新的标头
  3. 复制部分
    • 遍历节标题
    • 分配或复制部分数据
    • 使用新地址更新节标题
    • 设置每个部分的内存保护
  4. 搬迁项目基地(如适用)
    • 检查我们是否需要执行搬迁
    • 抵消需要更新的重定位
  5. 解决导入

自己做这些步骤

阅读和解析

我们将 LoadLibrary 传递给 DLL 的路径,因此该过程的第一步是从文件中读取数据并将其放入内存。Windows 提供了一个 API 调用来执行这个操作,CreateFile,但是我经常遇到这个调用的问题,需要程序以管理员模式运行,所以我只使用ifstream。

这导致了几行代码,我们以二进制模式打开文件,获取文件的大小,为要读取的内容分配空间,然后读取并关闭文件:

代码语言:javascript
复制
// Open file in binary mode
std::ifstream File(dllPath, std::ios::binary | std::ios::ate);

// Get the size of the program
size_t fileSize = File.tellg();

// Allocate space for the data
BYTE* data = new BYTE[fileSize];

// Reset position to the start of the file
File.seekg(0, std::ios::beg);

// Read all of the file into our data
File.read((char*)data, fileSize);

// Close the handle to the file
File.close();

一旦我们将文件放入内存中,我们就可以开始提取我们需要的数据。我们需要的第一个重要信息是DOS 标头:

代码语言:javascript
复制
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS 标头将位于我们刚刚读入的数据的开头,因此我们可以将数据键入为指向标头的指针:

代码语言:javascript
复制
// Get dos header
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)(data);

在 DOS 标头之后,我们有File 标头和Optional 标头。Windows 为我们提供了一个很好的数据结构,使我们可以根据 DOS 标头中定义的偏移量访问两者。所以此时,要获取 nt 标头,我们只需键入从数据开始到该结构的偏移量:

代码语言:javascript
复制
// Get nt headers
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(data + dosHeader->e_lfanew);

有了这个,我们现在可以进入下一步,分配内存。

分配内存

分配内存的第一步是弄清楚我们需要在我们的进程中分配多少内存。幸运的是,我们可以从 OptionalHeader 中弄清楚这一点。我们希望数据与系统的页面大小对齐,因此我们还可能必须根据图像大小分配更多内存:

代码语言:javascript
复制
// Declare the variable where we will store the system information
SYSTEM_INFO sysInfo;

// Get the native system information
GetNativeSystemInfo(&sysInfo);

// Get the image size aligned to the next largest multiple of the page size
size_t imageSize = (ntHeaders->OptionalHeader.SizeOfImage + sysInfo.dwPageSize - 1) & ~(sysInfo.dwPageSize - 1);

现在我们有了图像大小,我们可以使用本机 winapi 函数在程序中分配内存。首先,我们将尝试在标头中指定的图像库中分配内存,如果我们无法做到这一点,我们将让系统决定在哪里分配内存:

代码语言:javascript
复制
// Attempt to allocate memory at the image base
unsigned char* code = (unsigned char*)VirtualAlloc((LPVOID)(ntHeaders->OptionalHeader.ImageBase), imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

// If the result was null then let the system decide where to allocate
if (code == NULL) 
	code = (unsigned char*)VirtualAlloc(NULL, imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

在上面的代码中,我们使用reserve 标志来保留内存空间,并使用commit 标志来分配内存。

分配内存的最后一步是将标头复制到我们分配的内存中,然后使用分配内存的位置更新这些标头中的 ImageBase。这很重要,因为我们之前执行 VirtualAlloc 时可能无法在 ImageBase 分配内存:

代码语言:javascript
复制
// Copy the headers from the data into the allocated memory
memcpy(code, data, ntHeaders->OptionalHeader.SizeOfHeaders);

// Get the pointer to the allocated headers
PIMAGE_NT_HEADERS allocatedHeaders = (PIMAGE_NT_HEADERS)(code + dosHeader->e_lfanew);

// Update the image base to where we allocated our memory
allocatedHeaders->OptionalHeader.ImageBase = (uintptr_t)code;

复制部分

加载 DLL 的下一步是将这些部分复制到内存中。在复制数据时,我们通常需要注意两种不同类型的部分。第一个是其中包含数据的部分,这可能是字符串或代码或其他需要访问的东西,这些需要全部复制。其他部分是没有自己的数据的部分,但您需要为它们分配一定数量的空间。

为了复制这些部分,我们将获取第一个部分标题,然后我们将遍历所有部分。然后我们要么将原始数据复制到内存中,要么根据节的类型分配未初始化的数据。一旦我们复制了该部分,我们就会使用我们刚刚将我们的部分复制到的地址更新部分标题:

代码语言:javascript
复制
// Get first section header
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(allocatedHeaders);

// Loop through all sections
for (int i = 0; i < allocatedHeaders->FileHeader.NumberOfSections; i++, section++)
{
	// Get the physical address of the section in memory
	unsigned char* segmentAddress = code + section->VirtualAddress;

	// If the size of the data is 0 then we are "allocating" memory for a data section
	if (!(section->SizeOfRawData))
	{
		// Check if the SectionAlignment is greater than 0, semi redundent check but better safe than sorry
		if (ntHeaders->OptionalHeader.SectionAlignment > 0)
			// Set the SectionAlignment number of bytes to 0
			memset(segmentAddress, 0, ntHeaders->OptionalHeader.SectionAlignment);
	}
	// If not a data section then we need to copy all of the data over to it
	else
		// Copy the raw data into memory
		memcpy(segmentAddress, data + section->PointerToRawData, section->SizeOfRawData);

	// Update the address in the section header to point to our loaded section
	section->Misc.PhysicalAddress = (DWORD)((uintptr_t)segmentAddress & 0xffffffff);
}

通常,当 LoadLibrary 加载程序时,它会执行重定位,然后是导入,然后遍历各个部分并保护内存(设置为读取、写入和/或执行)。然而,这对我们的最终目标不起作用,因为我们想使用此代码将 dll 注入另一个进程。由于我们将此 dll 注入另一个进程,因此该进程将不得不进行导入解析。这意味着我们仍然可以在该进程中设置保护,但是,通常当您手动映射 dll 时,您很可能会做一些该进程确实不希望您这样做的事情,因此从内部设置保护可能有点问题. 我们不这样做的另一个原因

我不会在这一步遇到麻烦,而是要在这一步设置所有保护,并让所有内容都保持可写状态。如果您在初始内存分配中将所有内容设置为可执行文件,则可以跳过此步骤,但这再次使注入相当容易被发现。

我们在这段代码中的第一步是获取节,它的地址,它所在页面的开始,节大小,以及它的特征:

代码语言:javascript
复制
// Get the first section
section = IMAGE_FIRST_SECTION(allocatedHeaders);

// Get the physical address of the first section
LPVOID gAddress = (LPVOID)section->Misc.PhysicalAddress;

// Get the address of the first section aligned with the page size
LPVOID gAlignedAddress = (LPVOID)((uintptr_t)gAddress & ~(sysInfo.dwPageSize - 1));

// Get the section size
size_t gSize = GetSectionSize(allocatedHeaders, section);

// Get the section characteristics 
DWORD gCharacteristics = section->Characteristics;

// Set isLast to false
bool isLast = false;

IMAGE_FIRST_SECTION 由 windows 提供,而 GetSectionSize 是我们的功能之一:

代码语言:javascript
复制
// Gets the size of the section
size_t GetSectionSize(PIMAGE_NT_HEADERS headers, PIMAGE_SECTION_HEADER section) {
	// Set the size to the size of the raw data
	DWORD size = section->SizeOfRawData;

	// If the section has no raw data then set the size to the size of the data
	if (size == 0) {
		if (section->Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) {
			size = headers->OptionalHeader.SizeOfInitializedData;
		}
		else if (section->Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) {
			size = headers->OptionalHeader.SizeOfUninitializedData;
		}
	}

	// Return the size
	return (size_t)size;
}

在我们获得初始信息后,我们将遍历所有部分并获取每个部分的数据。如果该部分与上一部分在同一页面上,我们将把这些特征添加到我们的“全局”特征中,然后继续下一部分。否则,我们将去释放页面或设置部分的保护并继续下一部分:

代码语言:javascript
复制
// Loop through all sections
for (int i = 0; i < allocatedHeaders->FileHeader.NumberOfSections; i++, section++) {

	// Get all of the information for the current section
	LPVOID sectionAddress = (LPVOID)((uintptr_t)section->Misc.PhysicalAddress);
	LPVOID alignedAddress = (LPVOID)((uintptr_t)sectionAddress & ~(sysInfo.dwPageSize - 1));
	SIZE_T sectionSize = GetSectionSize(allocatedHeaders, section);

	// Check if the current section is on the same page as the previous section
	if (gAlignedAddress == alignedAddress || (uintptr_t)gAddress + gSize > (uintptr_t) alignedAddress) {
		// If it is then update the characteristics with those of the current section
		if ((section->Characteristics & IMAGE_SCN_MEM_DISCARDABLE) == 0 || (gCharacteristics & IMAGE_SCN_MEM_DISCARDABLE) == 0) {
			gCharacteristics = (gCharacteristics | section->Characteristics) & ~IMAGE_SCN_MEM_DISCARDABLE;
		}
		else {
			gCharacteristics |= section->Characteristics;
		}

		// Get the size from the start of the first section on the page up till the end of the most recent section
		gSize = (((uintptr_t)sectionAddress) + ((uintptr_t)sectionSize)) - (uintptr_t)gAddress;

		// Skip the rest of the loop
		continue;
	}

	// If the section can be discarded then free the memory
	if (gCharacteristics & IMAGE_SCN_MEM_DISCARDABLE) {
		// Check that a whole page is getting freed
		if (gAddress == gAlignedAddress &&
			(isLast ||
				allocatedHeaders->OptionalHeader.SectionAlignment == sysInfo.dwPageSize ||
				(gSize % sysInfo.dwPageSize) == 0)
			) {
			VirtualFree(gAddress, gSize, MEM_DECOMMIT);
		}
	}

	// Check if the section is executable and or readable, we ignore writeable since we are going to need
	// the sections to be writable when we do the imports once the dll is injected. The imports need to be 
	// done in the injected process while we want to do this in our injector so we need to make some exceptions.
	bool executable = (gCharacteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
	bool readable = (gCharacteristics & IMAGE_SCN_MEM_READ) != 0;

	// Array of allowed protections
	int ProtectionFlags[2][2] = {{PAGE_WRITECOPY, PAGE_READWRITE, }, {PAGE_EXECUTE_WRITECOPY, PAGE_EXECUTE_READWRITE,}};

	// Get the protection flag to use
	DWORD protection = ProtectionFlags[executable][readable];

	// check if we need to add the no cache flag to the protection
	if (gCharacteristics & IMAGE_SCN_MEM_NOT_CACHED) {
		protection |= PAGE_NOCACHE;
	}

	// Declare old protection
	DWORD oldProtection;

	// Change the protection of the section
	VirtualProtect(gAddress, gSize, protection, &oldProtection);

	// Set the new values
	gAddress = sectionAddress;
	gAlignedAddress = alignedAddress;
	gSize = sectionSize;
	gCharacteristics = section->Characteristics;

	// If this is the last section then set isLast to true
	if (i == allocatedHeaders->FileHeader.NumberOfSections - 1)
		isLast = true;
}

搬迁项目基地(如适用)

如果我们无法在原始 ImageBase 分配内存,我们将需要通过并将基本重定位更新到新地址。这里的第一步是看看我们是否甚至需要执行重定位,我们这样做的方式是我们将获得原始 ImageBase 和我们分配内存的基础之间的差异:

代码语言:javascript
复制
// Get the difference between the two ImageBases
ptrdiff_t ptrDiff = (ptrdiff_t)(allocatedHeaders->OptionalHeader.ImageBase - ntHeaders->OptionalHeader.ImageBase);

一旦我们知道我们需要执行重定位,我们就需要遍历所有重定位及其所有信息部分,如果重定位类型为 IMAGE_REL_BASED_HIGHLOW,则根据 ImageBase 之间的差异调整值:

代码语言:javascript
复制
	// Check if we need to perform any relocations
if (ptrDiff)
{
	// Get data directory
	PIMAGE_DATA_DIRECTORY directory = &(allocatedHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);

	// Get first base relocation
	PIMAGE_BASE_RELOCATION relocation = (PIMAGE_BASE_RELOCATION)(code + directory->VirtualAddress);

    // Loop through all relocations, check virtual address to see when to quit
    while (relocation->VirtualAddress > 0)
    {
        // Get the physical address of the relocation
        unsigned char* physicalAddress = code + relocation->VirtualAddress;

        // Get the first relocation information for the relocation
        unsigned short* relInfo = (unsigned short*)GetPointerOffset(relocation, sizeof(IMAGE_BASE_RELOCATION));

        // Loop through all relocation info
        for (int i = 0; i < ((relocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2); i++, relInfo++)
        {
            // Get the type of relocation
            int type = *relInfo >> 12;

            // Get the relocation offset
            int offset = *relInfo & 0xfff;

            // If the type is IMAGE_REL_BASED_HIGHLOW then perform our patch
            if (type == IMAGE_REL_BASED_HIGHLOW)
            {
                // Get the reference
                DWORD*  patch = (DWORD*)(physicalAddress + offset);

                // Adjust the reference based on the difference between the ImageBases
                *patch += (DWORD)ptrDiff;
            }
        }

        // Get the next relocation
        relocation = (PIMAGE_BASE_RELOCATION)GetPointerOffset(relocation, relocation->SizeOfBlock);
    }
}

GetPointerOffset 只是我在文件顶部定义的一个预处理器宏:

代码语言:javascript
复制
#define GetPointerOffset(data, offset) (void*)((uintptr_t)data + offset)

解决导入

这个过程的最后一步是解析导入,这需要在我们想要注入我们的 dll 的任何进程中完成,但开始我们只是要像我们的注入器是我们注入的进程一样行事。

首先,我们将遍历导入表并加载每个导入。一旦加载到内存中,我们将遍历所有 thunk 并设置所有函数调用的地址。对于这一部分,我们将只使用 LoadLibrary,因为解析所有相对路径(KERNEL32 等)会很麻烦,而且有点不必要。此代码如下所示:

代码语言:javascript
复制
// Get the entry import directory
PIMAGE_DATA_DIRECTORY directory = &(allocatedHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);

// Get the first import descriptor
PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(code + directory->VirtualAddress);

// While the descriptor has a name keep looping through them all
while (importDesc->Name)
{
	// Declare our two reference pointers
	uintptr_t* thunkRef;
	FARPROC* funcRef;

	// Get the handle to the import (using LoadLibrary since we don't want to have to resolve all of the relative paths)
	HMODULE handle = LoadLibraryA((LPCSTR)(code + importDesc->Name));

	// If original first think then use that virtual address, otherwise use first thunk address
	if (importDesc->OriginalFirstThunk)
		thunkRef = (uintptr_t*)(code + importDesc->OriginalFirstThunk);
	else
		thunkRef = (uintptr_t*)(code + importDesc->FirstThunk);

	// Get function reference
	funcRef = (FARPROC*)(code + importDesc->FirstThunk);

	// Loop through all thunks and set the value of the function reference
	for (; *thunkRef; thunkRef++, funcRef++)
	{
		if (IMAGE_SNAP_BY_ORDINAL(*thunkRef))
		{
			*funcRef = GetProcAddress(handle, (LPCSTR)IMAGE_ORDINAL(*thunkRef));
		}
		else
		{
			PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)(code + (*thunkRef));
			*funcRef = GetProcAddress(handle, (LPCSTR)&thunkData->Name);
		}
	}

	// increment the descriptor
	importDesc++;
}

解析导入表后,我们需要检查是否有 TLS 目录,然后附加所有这些 DLL。TLS 调用,也称为线程本地存储调用,只是在调用入口点之前执行的子例程。我们需要调用这些,因为我们将手动调用 dll 的入口点。幸运的是,此代码非常简单:

代码语言:javascript
复制
// If we have a TLS directory
if (allocatedHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].Size)
{
	// Get the directory
	IMAGE_TLS_DIRECTORY* tlsDirectory = (IMAGE_TLS_DIRECTORY*)(code + allocatedHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);

	// Get the first callback
	PIMAGE_TLS_CALLBACK* pCallback = (PIMAGE_TLS_CALLBACK*)(tlsDirectory->AddressOfCallBacks);

	// Loop through all callbacks
	for (; pCallback && (*pCallback); ++pCallback)
	{
		// Attach the dll
		PIMAGE_TLS_CALLBACK Callback = *pCallback;
		Callback(code, DLL_PROCESS_ATTACH, nullptr);
	}
}

调用入口点

现在我们已经将 DLL 加载到内存中,复制了部分,重新​​定位了基础,并解析了导入,是时候调用入口点了。为此,我们只需要获取入口点的地址,然后调用它:

代码语言:javascript
复制
// typedef the DLL entry function
typedef bool(WINAPI* DllEntryProc)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);

// Get the address for the entry 
DllEntryProc DllEntry = (DllEntryProc)(LPVOID)(code + allocatedHeaders->OptionalHeader.AddressOfEntryPoint);

// Call the entrypoint
(*DllEntry)((HINSTANCE)code, DLL_PROCESS_ATTACH, 0);

此时我们有以下代码:

当前代码

现在运行时,它会将我们传递给函数的任何 DLL 注入到当前进程中。但是,当被注入其他进程时,这将失败,因为该进程不知道如何调用 LoadLibrayA 或 GetProcAddress。因此,我们需要调整代码并将其拆分为两个函数,第二个函数复制到我们想要注入代码的进程中。

注入手动映射的 DLL

为了将我们的数据复制到另一个函数中,我们将不得不以几种不同的方式修改我们的代码:

  • 将所有内存修改函数转换为其外部版本并将进程句柄传递给它们
  • 在内部为数据的本地版本分配空间
  • 更改我们的部分加载和基本重定位以处理本地副本
  • 将导入分辨率移至另一个函数并剥离函数调用
  • 将我们数据的本地版本复制到其他进程
  • 用它需要的任何数据调用我们的 shellcode

转换内存修改函数

幸运的是,这第一步非常容易。无论我们在哪里有 VirtualAlloc、VirtualFree 和 VirtualProtect,我们都将在它们的末尾添加 Ex,并在开头添加一个参数,该参数是我们想要将代码注入到的任何进程的句柄:

代码语言:javascript
复制
unsigned char* code = (unsigned char*)VirtualAlloc((LPVOID)(ntHeaders->OptionalHeader.ImageBase), imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

变成:

代码语言:javascript
复制
unsigned char* code = (unsigned char*)VirtualAllocEx(hProc, (LPVOID)(ntHeaders->OptionalHeader.ImageBase), imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

我们这样做是因为我们需要这些操作发生在其他进程而不是我们自己的进程中。

为数据的本地版本分配内部空间

由于我们需要对数据、标题、节和重定位进行一些修改,因此我们需要数据的本地副本。为此,我们将分配内存,而不是将allocatedHeaders设置为基于代码,而是基于该本地副本:

代码语言:javascript
复制
// Attempt to allocate memory at the image base
unsigned char* code = (unsigned char*)VirtualAllocEx(hProc, (LPVOID)(ntHeaders->OptionalHeader.ImageBase), imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// If the result was null then let the system decide where to allocate
if (code == NULL)
	code = (unsigned char*)VirtualAllocEx(hProc, NULL, imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// Allocate space in the process for the data
unsigned char* localCode = (unsigned char*)VirtualAlloc(NULL, imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// Copy the headers from the data into the allocated memory
memcpy(localCode, data, ntHeaders->OptionalHeader.SizeOfHeaders);

// Get the pointer to the allocated headers
PIMAGE_NT_HEADERS allocatedHeaders = (PIMAGE_NT_HEADERS)(localCode + dosHeader->e_lfanew);

更新部分和重定位以修改本地副本

由于我们需要在本地副本上执行这些操作,我们需要遍历这段代码并确保我们正在从 localCode 而不是代码中偏移所有内容。在这些代码段中我们仍然应该引用代码的唯一地方是当我们在重定位代码的开头获得代码地址和标头中的图像基址之间的差异时。

将导入分辨率移至另一个函数并剥离函数调用

当我们解析导入表时,我们会遍历并将所有需要的导入加载到我们的进程中,然后更新我们的引用以指向任何导入的函数。这需要在我们的注入过程中完成,因为我们需要导入在该过程中。为此,我们将创建一个函数,将其复制到另一个进程并调用。

由于代码将在另一个进程中运行,我们需要注意不要在我们的函数中包含任何函数调用,因为这些不会指向我们在新进程中可以到达的地址。为了解决这个问题,我们将创建一个可以传递给 shellcode 函数的结构,该函数将包含我们加载的 dll 的基地址,然后指向其他进程可以访问的 loadLibrary 和 getProcAddress 的指针:

代码语言:javascript
复制
// LoadLibrary function 
typedef HMODULE(WINAPI tLoadLibrary)(LPCSTR);

// GetProcAddress function 
typedef FARPROC(WINAPI tGetProcAddress)(HMODULE, LPCSTR);

// Manually mapped data struct
typedef struct {
	unsigned char* code;
	tLoadLibrary* loadLibrary;
	tGetProcAddress* getProcAddress;
} MAN_MAP_DATA;

然后我们创建函数,复制代码,并使用指针而不是函数调用来调用这两个函数:

代码语言:javascript
复制
// Shellcode we inject into the function
HINSTANCE __stdcall ManMap(MAN_MAP_DATA * data)
{

	// Get dos header
	PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)(data->code);

	// Get the pointer to the allocated headers
	PIMAGE_NT_HEADERS allocatedHeaders = (PIMAGE_NT_HEADERS)(data->code + dosHeader->e_lfanew);

	// Get the entry import directory
	PIMAGE_DATA_DIRECTORY directory = &(allocatedHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);

	// Get the first import descriptor
	PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(data->code + directory->VirtualAddress);

	auto loadLibrary = data->loadLibrary;
	auto getProcAddress = data->getProcAddress;

	// While the descriptor has a name keep looping through them all
	while (importDesc->Name)
	{
		// Declare our two reference pointers
		uintptr_t* thunkRef;
		FARPROC* funcRef;

		// Get the handle to the import (using LoadLibrary since we don't want to have to resolve all of the relative paths)
		HMODULE handle = loadLibrary((LPCSTR)(data->code + importDesc->Name));


		// If original first think then use that virtual address, otherwise use first thunk address
		if (importDesc->OriginalFirstThunk)
			thunkRef = (uintptr_t*)(data->code + importDesc->OriginalFirstThunk);
		else
			thunkRef = (uintptr_t*)(data->code + importDesc->FirstThunk);

		// Get function reference
		funcRef = (FARPROC*)(data->code + importDesc->FirstThunk);

		// Loop through all thunks and set the value of the function reference
		for (; *thunkRef; thunkRef++, funcRef++)
		{
			if (IMAGE_SNAP_BY_ORDINAL(*thunkRef))
			{
				*funcRef = getProcAddress(handle, (LPCSTR)IMAGE_ORDINAL(*thunkRef));
			}
			else
			{
				PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)(data->code + (*thunkRef));
				*funcRef = getProcAddress(handle, (LPCSTR)&thunkData->Name);
			}
		}

		// increment the descriptor
		importDesc++;
	}

	 //If we have a TLS directory
	if (allocatedHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].Size)
	{
		// Get the directory
		IMAGE_TLS_DIRECTORY* tlsDirectory = (IMAGE_TLS_DIRECTORY*)(data->code + allocatedHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);

		// Get the first callback
		PIMAGE_TLS_CALLBACK* pCallback = (PIMAGE_TLS_CALLBACK*)(tlsDirectory->AddressOfCallBacks);

		// Loop through all callbacks
		for (; pCallback && (*pCallback); ++pCallback)
		{
			// Attach the dll
			PIMAGE_TLS_CALLBACK Callback = *pCallback;
			Callback(data->code, DLL_PROCESS_ATTACH, nullptr);
		}
	}

	// ****************************************************************************************************************************************************

	// typedef the DLL entry function
	typedef bool(WINAPI* DllEntryProc)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);

	// Get the address for the entry 
	DllEntryProc DllEntry = (DllEntryProc)(LPVOID)(data->code + allocatedHeaders->OptionalHeader.AddressOfEntryPoint);

	// Call the entrypoint
	(*DllEntry)((HINSTANCE)data->code, DLL_PROCESS_ATTACH, 0);

	// ****************************************************************************************************************************************************

	// Return the base address of our loaded DLL
	return (HINSTANCE)data->code;

}

// End position of shellcode, used to dynamically determine size of the function
DWORD ManMapEnd() { return 1; }

我们必须确保在 shellcode 中包含调用对流,并且在我们的 shellcode 之后有一个“函数”。第二个函数 ManMapEnd 将让我们动态地获取 shellcode 的大小,从而我们不必将任何大小硬编码到我们的程序中。

将我们数据的本地版本复制到其他进程

一旦我们完成了我们需要对本地代码做的所有事情,我们需要将它复制到我们之前分配的基地址中。这可以通过一个函数调用轻松完成:

代码语言:javascript
复制
// Copy the local code to the other process
WriteProcessMemory(hProc, code, localCode, imageSize, NULL);

用它需要的任何数据调用我们的 shellcode

我们需要做的最后一件事是初始化我们的结构,分配和复制该数据和 shellcode,然后调用 shellcode。幸运的是,这又一次相当简单,因为 WINAPI 为我们完成了所有繁重的工作:

代码语言:javascript
复制
// Initialize manually mapped data struct
MAN_MAP_DATA* manMapData = new MAN_MAP_DATA;

// Set the pointer of the code
manMapData->code = code;

// Get the pointer to load library
manMapData->loadLibrary = (tLoadLibrary*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");

// Get the pointer to get proc address
manMapData->getProcAddress = (tGetProcAddress*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetProcAddress");

// Allocate space for the manually mapped data
LPVOID man_map_data = VirtualAllocEx(hProc, NULL, sizeof(MAN_MAP_DATA), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// Allocate space for the shellcode
BYTE* man_map = (BYTE*)VirtualAllocEx(hProc, NULL, (DWORD)((ULONG_PTR)ManMapEnd - (ULONG_PTR)ManMap), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// Copy the data to the other function
WriteProcessMemory(hProc, man_map_data, manMapData, sizeof(MAN_MAP_DATA), NULL);

// Copy the shellcode to the other function
WriteProcessMemory(hProc, man_map, ManMap, (DWORD)((ULONG_PTR)ManMapEnd - (ULONG_PTR)ManMap), NULL);

// Create the thread in the other process
HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)man_map, man_map_data, 0, 0);

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 介绍
    • 什么是 DLL
    • 什么是DLL注入
    • 这篇文章的目的
  • 使用 LoadLibrary 和 CreateRemoteThread
    • 加载库
    • 创建远程线程
    • 查找进程的句柄
    • 潜入代码
  • 手动执行 LoadLibrary
    • LoadLibrary 究竟为我们做了什么
    • 自己做这些步骤
      • 阅读和解析
      • 分配内存
      • 复制部分
      • 搬迁项目基地(如适用)
      • 解决导入
      • 调用入口点
    • 注入手动映射的 DLL
      • 转换内存修改函数
      • 为数据的本地版本分配内部空间
      • 更新部分和重定位以修改本地副本
      • 将导入分辨率移至另一个函数并剥离函数调用
      • 将我们数据的本地版本复制到其他进程
      • 用它需要的任何数据调用我们的 shellcode
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档