文档章节

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

rise-worlds
 rise-worlds
发布于 2016/06/20 13:42
字数 1732
阅读 6
收藏 0

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

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

本文转载自:http://www.cnblogs.com/flying_bat/archive/2008/07/17/1245390.html

共有 人打赏支持
rise-worlds

rise-worlds

粉丝 2
博文 1755
码字总数 0
作品 0
深圳
程序员
私信 提问
iOS应用程序的脱壳实现原理浅析

应用程序加载过程 对于诸多逆向爱好者来说,给一个app脱壳是一项必做的事情。基于安全性的考虑,苹果对上架到appstore的应用都会进行加密处理,所以如果直接逆向一个从appstore下载的应用程序...

欧阳大哥2013
2018/10/23
0
0
iOS程序从Run到mian函数

一.点击Run开始 当你在Xcode里点击Run的时候. Xcode调用了GCC和LLVM编译器.帮你把你所写的工程进行了编译.过程如下. 1.预处理阶段 a.把存储在不同文件中的源程序聚合在一起.(#include的展开)...

Sunxxxxx丶
2018/04/13
0
0
静态链接、动态链接和csapp(ics)的比喻

我对动态链接共享库的理解是: 在静态链接中,链接器(ld)直接把被调用的静态库(.a)中的模块(.o)复制到每个调用该函数的可重定位目标模块(or say文件)中组成可执行目标文件。之后加载...

BeerBread134
2017/11/30
0
0
Linux动态库(一)

起因 博主在以Linux下做开发。在软件需求中,需要动态库带来的灵活性。 比如说博主主导的智能主机的开发。它需要支持很多种类的设备控制,如普通的开关灯、RGB灯、窗帘、百叶窗等等。我们将这...

临峰不畏
2016/05/08
42
0
LINUX-动态链接与静态链接对比(动态库和静态库)

一、库的基础概念: 在windows平台和linux平台下都大量存在着库。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。由于windows和linux的本质不同,因此二者库的二进...

sssssuuuuu666
2017/12/14
0
0

没有更多内容

加载失败,请刷新页面

加载更多

类加载机制过程

1.加载。 将代码转换成字节流加载进内存。加载完之后创建一个Class对象,这个对象是访问数据的入口。 2.验证。 JVM规范验证和代码逻辑验证。 3.准备 内存分配和初始化。对static修饰的类变量...

无精疯
14分钟前
1
0
next.js 提示 chunk styles [mini-css-extract-plugin]

会出现css 导入警告 导入两个插件 并在next.config.js 配置 yarn add webpack-filter-warnings-pluginyarn add mini-css-extract-plugin const FilterWarningsPlugin = require('webpack-......

一箭落旄头
22分钟前
1
0
AWS的自动部署codeploy 应用程序规范文件

codedeploy应用程序的规范文件 ECS平台上的应用规范文件: AppSpec file也可以是 YAML 或 JSON 格式的,可以直接写入控制台内的编辑器内。 AppSpec file用于指定: 用于将流量定向到新任务集...

守护-创造
29分钟前
0
0
Confluence 6 超过当前许可证期限进行升级

这个页面将会对你在进行 Confluence 升级的时候超过了当前许可证的期限进行升级的情况。 许可证警告 在升级的过程中,你将会在 Confluence 的应用程序日志(log file)中看到类似下面的错误提...

honeymoose
35分钟前
1
0
JS 调用Angularjs 的方法

// 1. 获取 Controllerlet appElement = document.querySelector('[data-ng-controller=MessagesCtrl]');let scope = angular.element(appElement).scope();// 2. 调用方法scope.l......

Moks角木
50分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部