从内存中加载动态库(一)

程序使用动态库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;
}

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

posted @ 2008-07-17 18:42  至尊王者  阅读(5759)  评论(0编辑  收藏  举报