zoukankan      html  css  js  c++  java
  • 从内存中加载动态库(一)

    程序使用动态库DLL一般分为隐式加载和显式加载两种,分别对应两种链接情况。本文主要讨论显式加载的技术问题。我们知道,要显式加载一个DLL,并取得其中导出的函数地址一般是通过如下步骤:
        (1) 用LoadLibrary加载dll文件,获得该dll的模块句柄;
        (2) 定义一个函数指针类型,并声明一个变量;
        (3) 用GetProcAddress取得该dll中目标函数的地址,赋值给函数指针变量;
        (4) 调用函数指针变量。

        这个方法要求dll文件位于硬盘上面。现在假设我们的dll已经位于内存中,比如通过脱壳、解密或者解压缩得到,能不能不把它写入硬盘文件,而直接从内存加载呢?答案是肯定的。经过多天的研究,非法操作了N次,修改了M个BUG,死亡了若干脑细胞后,终于有了初步的结果,下面做个总结与大家共享。

    一、加载的步骤

        由于没有相关的资料说明,只能凭借感觉来写。首先LoadLibrary是把dll的代码映射到exe进程的虚拟地址空间中,我们要实现的也是这个。所以先要弄清楚dll的文件结构。好在这个比较简单,它和exe一样也是PE文件结构,关于PE文件的资料很多,阅读一番后,基本上知道了必须做的几个工作:
        (1)判断内存数据是否是一个有效的DLL。这个功能通过函数CheckDataValide完成。原型是:
            BOOL CMemLoadDll::CheckDataValide(void* lpFileData, int DataLength);
        (2)计算加载该DLL所需的虚拟内存大小。这个功能通过函数CalcTotalImageSize完成。原型是:
            int CMemLoadDll::CalcTotalImageSize();
        (3)将DLL数据复制到所分配的虚拟内存块中。该功能通过函数CopyDllDatas完成。要注意段对齐。
            void CMemLoadDll::CopyDllDatas(void* pDest, void* pSrc);
        (4)修正基地重定位数据。这个功能通过函数DoRelocation完成。原型是:
            void CMemLoadDll::DoRelocation( void *NewBase);
        (5)填充该DLL的引入地址表。这个功能由函数FillRavAddress完成。原型是:
            BOOL CMemLoadDll::FillRavAddress(void *pImageBase);
        (6)根据DLL每个节的属性设置其对应内存页的读写属性。我这里做了简化,所有内存区域都设置成一样的读写属性。
        (7)调用入口函数DllMain,完成初始化工作。这一步我一开始忽略了,所以总是发现自己加载的dll和LoadLibrary加载的dll有些不同(我把整块内存区域保存到两个文件中进行比较,够晕的)。只是最近猜想到还需要这一步。
        (8)保存dll的基地址(即分配的内存块起始地址),用于查找dll的导出函数。从现在开始这个dll已经完全映射到了进程的虚拟地址空间,可以使用它了。
        (9)不需要dll的时候,释放所分配的虚拟内存。

    二、要说明的几个问题

       (1)目前CMemLoadDll仅仅针对win32 动态库,没有考虑mfc常规和扩展dll。
       (2)只考虑使用dll中的函数,对于导出类的dll,由于通常都是隐式链接,所以也没有考虑。导出变量的dll虽然也是隐式链接,但是通过查找函数的方法也可以找到该变量,不过在取值的时候一定要符合dll中对变量的定义,比如dll中导出的是一个int变量,则得到该变量在dll中的地址后,需要强制转换成int*指针,然后取值。
       (3)查找函数的功能通过函数
           FARPROC  CMemLoadDll::MemGetProcAddress(LPCSTR lpProcName);
    实现,参数是dll导出的函数(或者变量)的名字。这里必须注意函数名修饰,通常不加extern"C"的函数,编译以后在dll中导出的都是修饰名,比如:
        在dll头文件中: extern __declspec(dllexport) int nTestDll;
        在.dll中的导出符号变成 ?nTestDll@@3HA
       所以,为了能够找到我们需要的函数,必须在.h中添加extern "C"修饰。最好是给dll加一个def文件,里面明确给出每个函数的导出名字。
       (4)PE中的内容比较多,有些细节没有考虑。比如CheckDataValide函数中没有考虑dll对操作系统版本的要求。
       (5)PE文件中的节有很多种。可以从节表(或者叫做区块表)中一一找到。而且每个节的属性都不同。例如:.text, .data, .rsrc, .crt等等。由于这个代码基于手头已有的pe文件资料,对于不熟悉的节,在映射dll数据的时候没有考虑是否需要处理。
       (6)一开始把dll映射到进程的地址空间以后,我试图直接使用GetProcAddress查找函数。最初我认为LoadLibrary返回的HINSTANCE值是0x10000000,把它传递给GetProcAddress可以找到目标函数,而我也把dll映射到0x10000000这个地址,但是当我把这个值传递给GetProcAddress的时候,发现无法找到函数,用GetLastError得到错误码一看是无效句柄的错误,这才明白原来LoadLibrary在加载dll的时候,同时创建了一个句柄放入进程的句柄表,而我们要做这个工作是比较麻烦的,所以只能自己写一个查找函数。
       (7)释放dll所占据的虚拟内存,原来我使用
          VirtualFree((LPVOID)pImageBase, 0,MEM_FREE);
    后来发现有问题,应该使用 VirtualFree((LPVOID)pImageBase, 0, MEM_RELEASE);
       (8)MemGetProcAddress不仅支持通过函数名查找,还支持通过导出序号查找函数。例如下面的用法:
    DLLFUNCTION fDll = (DLLFUNCTION)a.MemGetProcAddress((LPCTSTR)1);

    三、创建测试用的DLL,工程的名字取"TestDll"

        用VC向导创建一个WIN32 DLL工程,里面选择“导出一些符号”,为了测试需要,对源代码进行如下修改:
    (1)头文件
        // This class is exported from the TestDll.dll
        class TESTDLL_API CTestDll {
            public:
    CTestDll(void);
        };
        extern TESTDLL_API int nTestDll;
        //要修改的地方,添加了extern "C" 和 char *参数:
        extern "C"  TESTDLL_API int fnTestDll(char *);
      (2)cpp文件
          a. 添加 #include "stdlib.h"
          b. DllMain中
      case DLL_PROCESS_DETACH:
       nTestDll = 12345;
       break;
          c. 初始化变量
             TESTDLL_API int nTestDll=654321;
          d. 修改函数
             TESTDLL_API int fnTestDll(char *p)
             {
              if(p == NULL)
          return nTestDll;
              else
          return atoi(p);
              }

    四、创建测试工程。使用一个dlg工程,测试代码如下:

        假设 DllNameBuffer里面保存有dll文件的路径
    CFile f;
    if(f.Open(DllNameBuffer,CFile::modeRead))
    {
      int FileLength = f.GetLength();
      void *lpBuf = new char[FileLength];
      f.Read(lpBuf, FileLength);
      f.Close();

      CMemLoadDll a;
      if(a.MemLoadLibrary(lpBuf, FileLength)) //加载dll到当前进程的地址空间
      {
       typedef  int (*DLLFUNCTION)(char *);
       DLLFUNCTION fDll = (DLLFUNCTION)a.MemGetProcAddress("fnTestDll");
       if(fDll != NULL)
       {
        MessageBox("找到函数!!");
        CString str;
        str.Format("Result is: %d & %d",fDll(NULL), fDll("100"));
        MessageBox(str);
       }
       else
       {
        DWORD err = GetLastError();
        CString str;
        str.Format("Error: %d",err);
        MessageBox(str);
       }
      }

      delete[] lpBuf;
    }

    五、加载类源代码。(在后续贴子里面给出)

    我最擅长从零开始创造世界,所以从来不怕失败,它最多也就让我一无所有。
  • 相关阅读:
    【线程控制:线程休眠】
    【线程调度-优先级】
    【多线程实现方案一:继承Thread 类】
    【多线程概述】
    【使用Mybatis-Generator自动生成Mapper、Model、Mapping相关文件】
    【springmvc集成mybatis框架】
    【UltraISO制作centos7系统安装盘】
    【己有原码, 为何还有反码和补码?】
    【原码, 反码, 补码的基础概念和计算方法】
    【数据类型】
  • 原文地址:https://www.cnblogs.com/flying_bat/p/1245390.html
Copyright © 2011-2022 走看看