冰琥珀 发表于 2016-5-27 00:34:36

dll内存加载的实现

        学过C/C++的同学应该都接触过库的加载函数LoadLibrary。这个API函数可以实现动态添加dll的效果。但是有个问题,就是用这个函数加载库之后,可以在模块中找到库的相关信息,这在正常情况下是没啥的。但是在非正常的情况下,这就有较大的缺陷。比如在写外挂的时候,需要往游戏进程中注入一个dll,如果用LoadLibrary函数来加载,加载之后在进程模块中就会看到这个dll的信息,这样只要那些搞游戏开发的有意识的扫下进程模块,你的这个库就立马暴露了。所以在外挂中,隐藏库是很有必要的,而内存加载就可以达到隐藏的目的。
        这里会涉及到PE文件的相关知识,由于篇幅问题,PE格式就不细说,这里只做了解,PE的格式如下图所示

        这图看着挺复杂,其实就是PE头+区块表+区块数据,具体的大家就自己去查资料吧。
        要加载库到内存中,首先我们得先读取库的数据,读取函数如下
//读取dll数据到内存
//szLibName                要读取的库文件
//pBuffer                用来获取数据的缓存(这个值位NULL则只是获取文件大小,文件大小由lSize返回)
//lSize                        读取的数据大小(文件大小)
//成功返回0,失败返回-1
int ReadLibrary(wchar_t *szLibName, void *pBuffer, uint64_t &lSize)
{
        int nResult = 0;
        LARGE_INTEGER li = { 0 };
        HANDLE hFile = INVALID_HANDLE_VALUE;

        hFile = CreateFileW(szLibName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        if (INVALID_HANDLE_VALUE == hFile){

                return ERR_OPEN_FILE;
        }

        li.LowPart = GetFileSize(hFile, (LPDWORD)&li.HighPart);
        //pBuffer为NULL则只是为了获取文件大小
        if (NULL == pBuffer){

                lSize = li.QuadPart;
                CloseHandle(hFile);
                return ERR_SUCCESS;
        }

        if (ReadFile(hFile, pBuffer, lSize, (LPDWORD)&lSize, NULL)) {

                nResult = ERR_SUCCESS;
        }
        else{

                nResult = ERR_READ_FILE;
        }
        CloseHandle(hFile);

        return nResult;
}

        这个函数比较简单,只是打开文件,获取文件大小,然后读取文件,关闭文件等。做的一些处理就是当传入的缓存为NULL时,表示要获取文件大小,这时先不进行文件的读取,而只是将文件大小返回,以便外部开辟相应大小的缓存来装载数据。当数据读取出来之后,我们先要判断这个文件是否是PE格式,代码如下
//判断当前文件是否是PE格式
int IsPEFile(void *pBuffer)
{
        uint32_t uResult = 0;
        PIMAGE_DOS_HEADER                pDosHeader = NULL;
        PIMAGE_FILE_HEADER                pFileHeader = NULL;
        PIMAGE_NT_HEADERS                pNTHeader = NULL;

        pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
        pNTHeader = (PIMAGE_NT_HEADERS)((char*)pBuffer + pDosHeader->e_lfanew);

        //PE格式的文件是以"MZ"开头,在pDosHeader->e_lfanew处的值是"PE\0\0"
        if (pDosHeader->e_magic == IMAGE_DOS_SIGNATURE && pNTHeader->Signature == IMAGE_NT_SIGNATURE){

                uResult = ERR_PE_FORMAT;
        }
        else{

                uResult = ERR_OTHERE_FORMAT;
        }

        return uResult;
}

        要想理解上面的代码,我们先来看Dos头的结构,如下
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;                  // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2;                  // Reserved words
    LONG   e_lfanew;                  // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;


                通过上面的PE文集格式的结构可知,在文件开始处,也就是Dos头有个"MZ"的标志,这个被定义为IMAGE_DOS_SIGNATURE,其值为0x5A4D,其实就是字符'M'和'Z'的ascii码值。在Dos头中,最后一个成员变量e_lfanew是NT头的RVA,即PE文件数据的首地址加上这个值就是NT头的首地址,NT头的格式如下
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;


        在NT头中有个Signature,这个成员变量是NT头的标志,被定义为IMAGE_NT_SIGNATURE,其值为0x00004550,其实就是"PE\0\0"的ascii码值。这里只是简单的判断了下这两个标志的值是否为定义的值,如果是,则认为这个文件的数据是PE格式,否则不是。
        判断了文件格式之后,接下来就是加载文件数据到内存了。加载到内存并不是在内存中开辟个空间,然后直接把数据写入就行了,在PE格式中,有加载后的内存偏移和文件偏移的区别,文件偏移就是数据保存在文件中使用的偏移,内存偏移就是dll被加载到内存后的偏移,这些偏移在PE结构中都可以读取出来。因此在加载之前,我们先要计算dll加载到内存后要占用多少空间,然后在开辟相应大小的空间来加载dll。计算空间的代码如下
//计算加载到内存后的数据大小
//pImage        读取到的库文件数据
//返回加载到内存后索要占用的内存大小
uint64_t GetImageSize(void *pImage)
{
        int i = 0;
        uint32_t uSizeOfOptional = 0;
        uint32_t uSectionNumber = 0;
        uint32_t uImagePage = 0;
        uint64_t lImageSize = 0;
        PIMAGE_DOS_HEADER                pDosHeader = NULL;
        PIMAGE_FILE_HEADER                pFileHeader = NULL;
        PIMAGE_NT_HEADERS                pNTHeader = NULL;
        PIMAGE_SECTION_HEADER        pSectionHeader = NULL;
        SYSTEM_INFOsysInfo = { 0 };

        //判断是不是PE格式的文件
        if (ERR_PE_FORMAT != IsPEFile(pImage)){

                return ERR_OTHERE_FORMAT;
        }

        //先获取DOS头
        pDosHeader = (PIMAGE_DOS_HEADER)pImage;
        //获取NT头指针
        pNTHeader = (PIMAGE_NT_HEADERS)((char*)pImage + pDosHeader->e_lfanew);
        //获取第一个section的指针
        pSectionHeader = IMAGE_FIRST_SECTION(pNTHeader);
        //计算映像大小(映像大小 = 最后一个section的RVA + SizeOfRawData)
        for (i = 0; i < pNTHeader->FileHeader.NumberOfSections; i ++) {

                if (pSectionHeader.VirtualAddress + pSectionHeader.SizeOfRawData > lImageSize){

                        lImageSize = pSectionHeader.VirtualAddress + pSectionHeader.SizeOfRawData;
                }
        }

        //获取当前系统的基本信息
        GetSystemInfo(&sysInfo);
        //计算映像要占用的内存页数
        uImagePage = lImageSize / sysInfo.dwPageSize;
        //如果当前映像大小不能被内存页大小整除,则表示最后一页数据没有占满
        //即lImageSize / sysInfo.dwPageSize得出的结果还有余数,所以uImagePage要加1
        if (0 != lImageSize % sysInfo.dwPageSize){

                uImagePage ++;
        }

        lImageSize = uImagePage * sysInfo.dwPageSize;

        return lImageSize;
}

        这段代码用到了NT头中的FileHeader这个成员变量,这是个IMAGE_FILE_HEADER结构,其结构如下
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;                        //区块的个数
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;


        在这个结构中NumberOfSections成员变量存储的是区块的个数,我们可以用这个值来控制查找各个区块的RVA和大小。
        上面给出的PE结构图可以看出,在NT头之后,跟着就是区块表,区块表后跟着的就是区块数据,最后面的那些COFF啥的一般都为空,可以不用管。所以我们要计算的加载后的大小就是NT头+区块表+区块数据。
        在上面的代码中,先用IMAGE_FIRST_SECTION来得到区块表的指针。既然PE文件后面都是区块数据,那么最后个区块的RVA+该区块的大小,不就是dll加载到内存后的大小了么(这里可能有点绕,大家仔细揣摩)。那怎么找到最后一个区块数据的RVA和它的大小呢?
        这里用到的方法很简单,逐一比较各个区块RVA+该区块大小的值,得到的最大值即可认为是最后一个区块数据的RVA+区块数据长度(因为当前区块的RVA>=前一个区块的RVA+区块大小),而最后一个区块数据的RVA+长度就是dll加载到内存中的大小。
        我们得到大小之后,要把这个大小转成内存页的倍数,所以要先找出内存页的大小,然后用当前得到的值除以内存叶大小,则可得到要占用多少内存页,但是还有不被内存页整除的情况,所以用这个大小对内存页求模,如果不为0表示不能被内存页整除,即所需的内存页数+1.
        得到大小之后,就是分配内存,加载数据到内存中,代码如下
//分配内存,加载数据到内存中
//pImage        分配的内存
//pBuffer        读取到的dll文件数据
//lSize                需要分配的内存大小
int LoadLibraryImage(void *&pImage, void *pBuffer, uint64_t lSize)
{
        uint32_t uResult = 0;
        uint32_t uPEHeaderLen = 0;
        uint32_t uSectionTableSize = 0;
        PIMAGE_DATA_DIRECTORY        pDataDir = NULL;
        PIMAGE_DOS_HEADER                pDosHeader = NULL;
        PIMAGE_FILE_HEADER                pFileHeader = NULL;
        PIMAGE_NT_HEADERS                pNTHeader = NULL;
        PIMAGE_OPTIONAL_HEADER        pOptionalHeader = NULL;
        PIMAGE_SECTION_HEADER        pSectionHeader = NULL;

        pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
        pNTHeader = (PIMAGE_NT_HEADERS)((char*)pDosHeader + pDosHeader->e_lfanew);
        pSectionHeader = IMAGE_FIRST_SECTION(pNTHeader);
        uPEHeaderLen = (char*)pSectionHeader - (char*)pDosHeader;
        pImage = VirtualAlloc(NULL, lSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (NULL == pImage){

                return ERR_MALLOC;
        }
        ZeroMemory(pImage, lSize);
        //先写入PE头
        MoveMemory(pImage, pBuffer, uPEHeaderLen);
        //写入区块表
        uSectionTableSize = pNTHeader->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);
        MoveMemory((char*)pImage + uPEHeaderLen, (char*)pBuffer + uPEHeaderLen, uSectionTableSize);
        //写入区块数据
        for (int i = 0; i < pNTHeader->FileHeader.NumberOfSections; i ++) {

                MoveMemory((char*)pImage + pSectionHeader.VirtualAddress, (char*)pBuffer + pSectionHeader.PointerToRawData, pSectionHeader.SizeOfRawData);
        }

        return uResult;
}


        上面这段代码实现的是分配内存,然后往内存中写入数据。由于PE头和区块表没有文件偏移和内存偏移的问题,所以可以把它们直接写入内存即可。但是区块数据有特定的RVA,所以我们就根据其RVA来加载。这样把所有的数据都加载到内存之后,加载的环节就结束了,接下来就是做加载后的修补工作了。
        加载数据到内存之后,我们要对输入表进行修复,代码如下
//修复输入表
//pImage   PE文件加载到内存中的地址
int ImportRepair(void *pImage)
{
        char *szDllName = NULL;
        uint32_t uImportAddr = 0;
        uint32_t uImportSize = 0;
        uint32_t uExportAddr = 0;
        uint32_t uExportSize = 0;
        uint32_t *pOrgThunk = NULL;
        uint32_t *pFirstThunk = NULL;
        HMODULEhModule = NULL;
        PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL;
        PIMAGE_DATA_DIRECTORY        pImport = NULL;
        PIMAGE_DOS_HEADER                pDosHeader = NULL;
        PIMAGE_NT_HEADERS                pNTHeader = NULL;
        PIMAGE_IMPORT_BY_NAME   pImportName = NULL;

        pDosHeader = (PIMAGE_DOS_HEADER)pImage;
        pNTHeader = (PIMAGE_NT_HEADERS)((char*)pDosHeader + pDosHeader->e_lfanew);
        pImport = &pNTHeader->OptionalHeader.DataDirectory;
        if (NULL == pImport || 0 == pImport->VirtualAddress){

                return ERR_INVALID_IMPORT;
        }
        pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((char*)pDosHeader + pImport->VirtualAddress);
        uImportSize = pImport->Size;
        //重写输入表
        for (;;) {

                //获取dll名
                szDllName = (char*)pDosHeader + pImportDesc->Name;
                hModule = LoadLibraryA(szDllName);
                if (NULL == hModule){

                        return ERR_LOAD_LIB;
                }
                pOrgThunk = (uint32_t *)((char*)pDosHeader + pImportDesc->OriginalFirstThunk);
                pFirstThunk = (uint32_t*)((char*)pDosHeader + pImportDesc->FirstThunk);

                while (*pOrgThunk){

                        //最高位为1,表示函数以序号方式输入,低31位是函数序号
                        if (*pOrgThunk & 0x80000000){

                                *pFirstThunk = (uint32_t)GetProcAddress(hModule, (char*)(*pOrgThunk & 0xffff));
                        }
                        else{

                                pImportName = (PIMAGE_IMPORT_BY_NAME)((char*)pDosHeader + *pOrgThunk);
                                *pFirstThunk = (uint32_t)GetProcAddress(hModule, pImportName->Name);
                        }

                        //无法获取函数地址则退出
                        if (0 == *pFirstThunk){

                                FreeLibrary(hModule);
                                return ERR_PROC_ADDR;
                        }

                        pOrgThunk++;
                        pFirstThunk++;
                }

                pImportDesc++;
                if (0 == *(uint32_t*)pImportDesc){

                        break;
                }
        }

        return ERR_SUCCESS;
}


        要修复输入表,就要找到输入表的指针。这个得通过IMAGE_OPTIONAL_HEADER32结构来获取。先来看IMAGE_OPTIONAL_HEADER32结构
typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory;
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;


        这个结构的最后一个成员DataDirectory是个数组,这个数组中存储的是输入表、输出表、重定位、资源等结构的RVA。接下来介绍下输入表的结构,如下
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
      DWORD   Characteristics;            // 0 for terminating null import descriptor
      DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                          // -1 if bound, and real date\time stamp
                                          //   in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                          // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;               // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;


        输入表的结构比较简单,只有5个成员(开头是个联合),我们只需要关注其中的3个成员即可。其中Name是该dll中依赖的其他dll的名称的RVA,而OriginalFirstThunk和FirstThunk比较重要,都是IMAGE_THUNK_DATA 结构,如下所示
typedef struct _IMAGE_THUNK_DATA32 {
    union {
      DWORD ForwarderString;      // PBYTE
      DWORD Function;             // PDWORD
      DWORD Ordinal;
      DWORD AddressOfData;      // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;


        这个结构是个联合,有着多种意义。当该结构的最高位为1时,表示函数以序号的方式输入,则其低31位为函数的序号,当其最高位为0时,表示函数以字符串类型的方式输入,其低31未为指向IMAGE_IMPORT_BY_NAME的RVA。而这里的OriginalFirstThunk和FirstThunk,其中前者是不可改写的,后者是在加载的时候需要往里填入输入函数地址的。即我哦们这里的输入表修复其实就是往这里面填入输入函数地址。填入的方式为先加载库,根据OriginalFirstThunk来得到函数名或者序号,然后用GetProcAddress来获取函数地址,将得到的地址写入FirstThunk,直到加载完所有的输入函数地址即可。输入表修复了之后,我们接下来要修正重定位了。
        先说为啥要做重定位。链接器生成PE文件时有个默认的装载基地址,一般情况下,PE文件都会被加载到这个默认的基地址处,这样就不需要重定位,但是当PE文件被加载到其他地址处时,链接器所登记的地址就不正确了,这样就需要通过重定位来修正。修正重定位的代码如下
//基址重定位
//pImage   PE文件加载到内存中的地址
int BaseRelocation(void *pImage)
{
        uint32_t                                uSizeOfBlock = 0;
        uint16_t                                *pTypeOffset = NULL;
        uint32_t                                *pRelocAddr = NULL;
        PIMAGE_DOS_HEADER                pDosHeader = NULL;
        PIMAGE_NT_HEADERS                pNTHeader = NULL;
        PIMAGE_BASE_RELOCATION        pBaseRelocation = NULL;

        pDosHeader = (PIMAGE_DOS_HEADER)pImage;
        pNTHeader = (PIMAGE_NT_HEADERS)((char*)pDosHeader + pDosHeader->e_lfanew);
        pBaseRelocation = (PIMAGE_BASE_RELOCATION)((char*)pDosHeader + pNTHeader->OptionalHeader.DataDirectory.VirtualAddress);
        while (pBaseRelocation->VirtualAddress != 0){

                uSizeOfBlock = pBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION);
                pTypeOffset = (uint16_t*)((uint32_t)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));
                for (int i = 0; i < uSizeOfBlock / 2; i ++) {

                        //类型是IMAGE_REL_BASED_HIGHLOW(高4位表示类型)
                        if (IMAGE_REL_BASED_HIGHLOW == ((*pTypeOffset & 0xf000) >> 12)){

                                pRelocAddr = (uint32_t*)((char*)pDosHeader + pBaseRelocation->VirtualAddress + (*pTypeOffset & 0xfff));
                                *pRelocAddr = (uint32_t)((uint32_t)pDosHeader - pNTHeader->OptionalHeader.ImageBase + *pRelocAddr);
                        }

                        pTypeOffset++;
                }

                pBaseRelocation = (PIMAGE_BASE_RELOCATION)((uint32_t)pBaseRelocation + pBaseRelocation->SizeOfBlock);
        }

        return ERR_SUCCESS;
}


        这里先来了解下重定位的结构
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//WORD    TypeOffset;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;


        这个结构很简单,但是要注意的是,这结构很面跟着一个数组,这个数组每个元素占2字节,其中高4位表示重定位类型,低12位表示重定位地址。我们要做的就是取到重定位数据的指针(和获取输入表指针方式一样),然后计算重定位的项数,最后修改重定位地址,重定位地址的修改方式为库加载到内存的地址+当前重定位地址-默认的基址。
        重定位修正之后,我们只需要得到库的入口地址,然后调用即可。到了这里,库加载的流程算了结束了,加载的代码如下
HMODULE       MemLoadLibrary(wchar_t *szLibName)
{
        uint32_t nResult = 0;
        uint64_t lSize = 0;
        char *pLibBuf = NULL;
        char *pImage = NULL;
        DLLMAIN pfnDllMain = NULL;

        nResult = ReadLibrary(szLibName, NULL, lSize);

        if (ERR_SUCCESS != nResult){

                return (HMODULE)ERR_LOAD_LIB;
        }
       
        pLibBuf = (char*)malloc(lSize);
        if (NULL == pLibBuf) {

                return (HMODULE)ERR_LOAD_LIB;
        }

        memset(pLibBuf, 0, lSize);
        nResult = ReadLibrary(szLibName, pLibBuf, lSize);
        lSize = GetImageSize(pLibBuf);
        if (LoadLibraryImage((void*&)pImage, pLibBuf, lSize)) {

                free(pLibBuf);
                if (NULL != pImage){

                        VirtualFree(pImage, 0, MEM_RELEASE);
                        pImage = NULL;
                }

                return (HMODULE)ERR_LOAD_LIB;
        }
        ImportRepair(pImage);
        BaseRelocation(pImage);
        //获取DllMain函数的地址
        pfnDllMain = (DLLMAIN)GetDllMain(pImage);
        //调用DllMain函数
        pfnDllMain((HMODULE)pImage, DLL_PROCESS_ATTACH, NULL);

        return (HMODULE)pImage;
}


        库加载了之后,我们还可以提供一个获取函数地址的函数接口,这样当库加载了之后,我们可以通过这个接口来获取函数地址。代码如下
void*        MemGetProcAddress(HMODULE hModule, char *szFuncName)
{
        uint32_t uFuncAddr = 0;
        uint16_t uFuncAddrIndex = 0;
        char *szExportFuncName = NULL;
        uint32_t *pAddrOfFunctions = NULL;
        uint32_t *pAddrOfName = NULL;
        uint32_t *pAddrOfNameOrdinals = NULL;
        PIMAGE_DOS_HEADER                        pDosHeader = NULL;
        PIMAGE_NT_HEADERS                        pNTHeader = NULL;
        PIMAGE_EXPORT_DIRECTORY                pExportDir = NULL;

        pDosHeader = (PIMAGE_DOS_HEADER)hModule;
        pNTHeader = (PIMAGE_NT_HEADERS)((char*)hModule + pDosHeader->e_lfanew);
        pExportDir = (PIMAGE_EXPORT_DIRECTORY)((char*)hModule + pNTHeader->OptionalHeader.DataDirectory.VirtualAddress);
        pAddrOfFunctions = (uint32_t*)((char *)hModule + pExportDir->AddressOfFunctions);
        pAddrOfName = (uint32_t*)((char *)hModule + pExportDir->AddressOfNames);
        pAddrOfNameOrdinals = (uint32_t *)((char *)hModule + pExportDir->AddressOfNameOrdinals);
        if (NULL == pExportDir){

                return NULL;
        }

        for (int i = 0; i < pExportDir->NumberOfNames; i ++) {

                if (pAddrOfName != 0){

                        szExportFuncName = (char*)((char*)hModule + pAddrOfName);
                        if (!strcmp(szExportFuncName, szFuncName)) {

                                uFuncAddrIndex = pAddrOfNameOrdinals;
                                uFuncAddr = pAddrOfFunctions;
                                break;
                        }
                }
        }

        return ((char*)hModule + uFuncAddr);
}


        这个函数的流程是找到输出表,然后在输出表中找到要导出的函数名,根据函数名来取出函数地址,然后将函数地址返回。最后则是释放加载的库,代码如下
void        MemFreeLibrary(HMODULE hModule)
{
        PIMAGE_DOS_HEADER                        pDosHeader = NULL;
        PIMAGE_NT_HEADERS                        pNTHeader = NULL;
        DLLMAIN pfnDllMain = NULL;

        pDosHeader = (PIMAGE_DOS_HEADER)hModule;
        pNTHeader = (PIMAGE_NT_HEADERS)((char*)hModule + pDosHeader->e_lfanew);
        //获取DllMain函数的地址
        pfnDllMain = (DLLMAIN)GetDllMain((char*)hModule);
        if (NULL != pfnDllMain){

                pfnDllMain(hModule, DLL_PROCESS_DETACH, NULL);
        }

        VirtualFree((LPVOID)hModule, 0, MEM_RELEASE);
}


        到了这里,关于内存加载的流程和代码也介绍完了,想要把过程弄明白,需要有一定的PE格式的知识,建议大家边看先熟悉PE的知识,再来看这个,效果会更好
        附件里式写好的代码,感兴趣的可以拿去玩玩

小圈圈 发表于 2016-5-27 07:24:31

确切的说学完C/C++还不知道LoadLibrary这个函数,要学完'VC++或者Windows系统编程才知道

小龙 发表于 2016-5-27 10:24:49

我是来水经验的……

H.U.C-Star 发表于 2016-5-27 11:47:20

看懂你的文章还得学几年?告诉我!:'(

wanmznh 发表于 2016-5-27 12:01:49

a136 发表于 2016-5-27 12:58:46

我是来水经验的……

perble 发表于 2016-5-27 14:21:37

非常感谢

Micah 发表于 2016-5-27 15:00:12

支持,看起来还是可以的

人=族 发表于 2016-5-27 15:02:41

不觉明历

borall 发表于 2016-5-27 15:23:09

我是来水经验的……
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: dll内存加载的实现