将可执行文件加载到当前进程的内存中,然后执行它

Loading an executable into current process's memory, then executing it

首先,这仅用于教育目的。我绝不是专家。

我的第一门编程语言是 python。在 python 中,你有 exec() 和 eval() 函数,它们允许你 运行 任意字符串作为代码。现在我正在学习C++和汇编。我注意到,当我第一次开始使用 C++ 时,没有与上述功能等效的功能,这是因为 C++ 是一种编译语言。这让我想知道是否有一种方法可以让可执行文件编写 C++ 代码、调用编译器并将生成的字节码复制到内存中以动态修改程序的功能。当然,这是不切实际的,从本质上讲,有更好的方法可以实现我想要的。最后,我开始学习汇编,这让我对字节码到底是什么以及它是如何工作有了一些了解。这促使我回到这个概念;将其视为挑战和机遇。

大致思路如下:

程序A有可执行文件程序B作为资源(例如)。

程序A想在它自己的地址space执行程序B(在修改它或任何它想做的事情之后)。

程序 A 分配 space(当然有适当的权限)。

程序A拷贝程序B的字节码到分配的space.

程序 A 解析程序 B 的导入。

程序A将执行转移到程序B(可以将执行转移回程序A等)。


这是我到目前为止的基本代码:

#include <iostream>
#include <windows.h>
#include <winternl.h>

DWORD Rva2Offset(DWORD rva, PIMAGE_SECTION_HEADER psh, PIMAGE_NT_HEADERS pnt)
{
    size_t i = 0;
    PIMAGE_SECTION_HEADER pSeh;
    if (rva == 0)
    {
        return (rva);
    }
    pSeh = psh;
    for (i = 0; i < pnt->FileHeader.NumberOfSections; i++)
    {
        if (rva >= pSeh->VirtualAddress && rva < pSeh->VirtualAddress +
            pSeh->Misc.VirtualSize)
        {
            break;
        }
        pSeh++;
    }
    return (rva - pSeh->VirtualAddress + pSeh->PointerToRawData);
}


int main(int argc, char* argv[])
{

    PIMAGE_DOS_HEADER pIDH;
    PIMAGE_NT_HEADERS pINH;
    PIMAGE_SECTION_HEADER pISH;

    PVOID image, mem, base;
    DWORD i, read, nSizeOfFile;
    HANDLE hFile;

    if (argc != 2)
    {
        printf("\nNot Enough Arguments\n");
        return 1;
    }

    printf("\nOpening the executable.\n");

    hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("\nError: Unable to open the executable. CreateFile failed with error %d\n", GetLastError());
        return 1;
    }

    nSizeOfFile = GetFileSize(hFile, NULL);

    image = VirtualAlloc(NULL, nSizeOfFile, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // Allocate memory for the executable file

    if (!ReadFile(hFile, image, nSizeOfFile, &read, NULL)) // Read the executable file from disk
    {
        printf("\nError: Unable to read the replacement executable. ReadFile failed with error %d\n", GetLastError());
        return 1;
    }

    CloseHandle(hFile); // Close the file handle

    pIDH = (PIMAGE_DOS_HEADER)image;

    if (pIDH->e_magic != IMAGE_DOS_SIGNATURE) // Check for valid executable
    {
        printf("\nError: Invalid executable format.\n");
        return 1;
    }

    pINH = (PIMAGE_NT_HEADERS)((LPBYTE)image + pIDH->e_lfanew); // Get the address of the IMAGE_NT_HEADERS

    printf("\nAllocating memory in child process.\n");

    mem = VirtualAlloc((PVOID)pINH->OptionalHeader.ImageBase, pINH->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // Allocate memory for the executable image

    if (!mem)
    {
        mem = VirtualAlloc(NULL, pINH->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // Allow it to pick its own address
    }

    if ((DWORD)mem != pINH->OptionalHeader.ImageBase)
    {
            printf("\nProper base could not be reserved.\n");

            return 1;
    }

    printf("\nMemory allocated. Address: %#X\n", mem);

    printf("\nResolving Imports\n");


    if (pINH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size != 0)
    {
        PIMAGE_SECTION_HEADER pSech = IMAGE_FIRST_SECTION(pINH);

        PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)image + Rva2Offset(pINH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress, pSech, pINH));
        LPSTR libname;
        size_t i = 0;
        // Walk until you reached an empty IMAGE_IMPORT_DESCRIPTOR
        while (pImportDescriptor->Name != NULL)
        {
            printf("Library Name   :");
            //Get the name of each DLL
            libname = (PCHAR)((DWORD_PTR)image + Rva2Offset(pImportDescriptor->Name, pSech, pINH));
            printf("%s\n", libname);

            HMODULE libhandle = GetModuleHandle(libname);
            if(!libhandle)
                libhandle = LoadLibrary(libname);

            PIMAGE_THUNK_DATA nameRef = (PIMAGE_THUNK_DATA)((DWORD_PTR)image + Rva2Offset(pImportDescriptor->Characteristics, pSech, pINH));
            PIMAGE_THUNK_DATA symbolRef = (PIMAGE_THUNK_DATA)((DWORD_PTR)image + Rva2Offset(pImportDescriptor->FirstThunk, pSech, pINH));
            for (; nameRef->u1.AddressOfData; nameRef++, symbolRef++)
            {
                if (nameRef->u1.AddressOfData & 0x80000000)
                {
                    symbolRef->u1.AddressOfData = (DWORD)GetProcAddress(libhandle, MAKEINTRESOURCE(nameRef->u1.AddressOfData));
                }
                else
                {
                    PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)image + Rva2Offset(nameRef->u1.AddressOfData, pSech, pINH));
                    symbolRef->u1.AddressOfData = (DWORD)GetProcAddress(libhandle, (LPCSTR)&thunkData->Name);
                }
            }
            pImportDescriptor++; //advance to next IMAGE_IMPORT_DESCRIPTOR
            i++;

        }
    }

    printf("\nWriting executable image into child process.\n");

    memcpy(mem, image, pINH->OptionalHeader.SizeOfHeaders); // Write the header of the executable

    for (i = 0; i<pINH->FileHeader.NumberOfSections; i++)
    {
        pISH = (PIMAGE_SECTION_HEADER)((LPBYTE)image + pIDH->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i*sizeof(IMAGE_SECTION_HEADER)));
        memcpy((PVOID)((LPBYTE)mem + pISH->VirtualAddress), (PVOID)((LPBYTE)image + pISH->PointerToRawData), pISH->SizeOfRawData); //Write the remaining sections
    }

    if (pINH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size && (pINH->OptionalHeader.ImageBase != (DWORD)mem))
    {
        printf("\nBase relocation.\n");

        DWORD i, num_items;
        DWORD_PTR diff;
        IMAGE_BASE_RELOCATION* r;
        IMAGE_BASE_RELOCATION* r_end;
        WORD* reloc_item;

        diff = (DWORD)mem - pINH->OptionalHeader.ImageBase; //Difference between memory allocated and the executable's required base.
        r = (IMAGE_BASE_RELOCATION*)((DWORD)mem + pINH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); //The address of the first I_B_R struct 
        r_end = (IMAGE_BASE_RELOCATION*)((DWORD_PTR)r + pINH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size - sizeof(IMAGE_BASE_RELOCATION)); //The addr of the last

        for (; r<r_end; r = (IMAGE_BASE_RELOCATION*)((DWORD_PTR)r + r->SizeOfBlock))
        {
            reloc_item = (WORD*)(r + 1);
            num_items = (r->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

            for (i = 0; i<num_items; ++i, ++reloc_item)
            {
                switch (*reloc_item >> 12)
                {
                case IMAGE_REL_BASED_ABSOLUTE:
                    break;
                case IMAGE_REL_BASED_HIGHLOW:
                    *(DWORD_PTR*)((DWORD)mem + r->VirtualAddress + (*reloc_item & 0xFFF)) += diff;
                    break;
                default:
                    return 1;
                }
            }
        }
    }

    DWORD entrypoint = (DWORD)((LPBYTE)mem + pINH->OptionalHeader.AddressOfEntryPoint); 

    printf("\nNew entry point: %#X\n", entrypoint);

    VirtualFree(image, 0, MEM_RELEASE); // Free the allocated memory

    __asm jmp entrypoint

    return 0;
}

更新:此代码大部分时间都有效。它似乎在复杂的程序上失败了,截至目前,我不确定为什么。对于那些想知道的人:出现 & 0x80000000 的原因是因为该位表示您要将两个低位字节视为序数。所以我使用 MAKEINTRESOURCE 相应地转换地址。

我曾经做过类似的事情。首先,您应该用 C(++) 编写非常简单的函数。例如:

int add(a, b) {
     return a + b;
}

然后将其编译为目标文件(不要link)。然后从目标文件中复制可执行代码并尝试将其加载到内存中。

如果您使用的是可执行文件,则必须解析其中的信息。此外,您还必须 linking.

如果你想加载任意代码(相当模糊的表达但让我们使用它)那么你可以制作一个DLL。然后,在 运行 时,您可以让 LoadLibary 加载它,并让 GetProcAddress 获取指向 DLL 导出的函数的函数指针。

我认为这是您在 C/C++ 中可以做的最接近您所描述的事情。

你的代码是一个好的开始,但你遗漏了一些东西。

正如您提到的,首先是解析导入。你说的好像是对的,但我从来没有像你这样手动做过,所以我不知道细节。程序可以在不解析导入的情况下运行,但前提是您不使用任何导入函数。此处您的代码失败,因为它试图访问尚未解析的导入;函数指针包含 0x4242 而不是解析地址。

第二件事是搬家。为简单起见,PE executable 是位置独立的(可以在任何基地址工作),即使代码不是。为实现这一点,该文件包含一个重定位 table,用于调整所有依赖于图像位置的数据。如果您可以在首选地址(pINH->OptionalHeader.ImageBase)加载,则此点是可选的,但这意味着如果您使用重定位table,则可以在任何地方加载图像,并且可以省略第一个参数VirtualAlloc(并删除相关检查)。

如果您还没有找到有关导入解析和重定位的更多信息,可以在 this article 中找到。您可以找到许多其他资源。

此外,正如 marom 的回答中提到的,您的程序基本上就是 LoadLibrary 所做的,因此在更实际的情况下,您可以改用此功能。