【问题标题】:Loading an executable into current process's memory, then executing it将可执行文件加载到当前进程的内存中,然后执行它
【发布时间】:2016-03-09 07:10:24
【问题描述】:

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

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

大致思路如下:

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

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

程序 A 分配空间(当然具有适当的权限)。

程序 A 将程序 B 的字节码复制到分配的空间中。

程序 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 来相应地转换地址。

【问题讨论】:

  • 恭喜,您发明了带有运行时链接的动态库。 :D 或 JIT 编译。使用 LLVM 是 C++ 程序 JIT 编译某些机器代码并运行它的常用方法。 (而不是单独的编译器,您将编译器链接为库)。赞成,因为听起来你尝试了一些东西。如果你搜索过 IDK,但如果你不知道“即时编译”这个短语,可能很难找到。
  • 我会调查的,谢谢你的建议。
  • 你搞定了吗?

标签: c++ windows x86 inline-assembly portable-executable


【解决方案1】:

您的代码是一个好的开始,但您缺少一些东西。

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

第二件事是搬迁。为了简单起见,PE 可执行文件是位置无关的(可以在任何基地址上工作),即使代码不是。为了完成这项工作,该文件包含一个重定位表,用于调整依赖于图像位置的所有数据。如果可以在首选地址(pINH-&gt;OptionalHeader.ImageBase)加载,这点是可选的,但这意味着如果使用重定位表,则可以在任何地方加载图像,并且可以省略VirtualAlloc的第一个参数(并删除相关检查)。

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

另外,正如 marom 的回答中提到的,你的程序基本上是 LoadLibrary 所做的,所以在更实际的情况下,你会改用这个函数。

【讨论】:

  • 感谢您的链接。现在我正在解决导入问题。
  • @Alex 我看了你的新代码。你现在的问题是由于不幸的演员。使用(PIMAGE_THUNK_DATA)image,您将图像转换为指向结构的指针,并且递增指针不会正确递增。 (DWORD*)ptr + 4 将 ptr 增加 4*4,因为 DWORD 是 4 个字节。您对DWORD_PTR 的强制转换可以工作,因为DWORD_PTR 实际上被定义为一个整数,而不是一个指针。在添加偏移量之前,您应该始终转换为 DWORD_PTRchar*
【解决方案2】:

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

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

【讨论】:

  • 我认为这当然是最实用的方法,但如果我这样做,文件必须保存在磁盘上,这对于大多数情况来说都很好。我正在尝试将子可执行文件作为主要可执行文件中的资源,主要可执行文件可以在不将文件保存到硬盘的情况下运行。
【解决方案3】:

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-14
    • 2019-08-17
    • 1970-01-01
    • 2015-10-16
    • 2021-04-28
    相关资源
    最近更新 更多