DLL注入技术

原文链接:https://blog.csdn.net/Sagittarius_Warrior/article/details/52164204

一、DLL注入的方法

        DLL注入的方法有很多,《Windows核心编程》中介绍了如下几种:

1)使用注册表来注入DLL;

2)使用Windows挂钩来注入DLL;

3)使用远程线程来注入DLL;

4)使用木马DLL来注入DLL;

5)把DLL作为调试器来注入DLL;

6)使用CreateProcess来注入DLL;

        在本系列文章中,我将先介绍”远程线程“,后介绍”Windows挂钩“,其他的略过。

 

二、远程线程注入DLL的步骤

        《Windows核心编程》对”远程线程注入DLL“的步骤总结为以下7步,可以对照”InjLib工程“中的”InjLibW函数“中的代码进行学习。

1)用VirtualAllocEx函数在远程进程的地址空间中分配一块内存。

2)用WriteProcessMemory函数把DLL的路径名复制到第一步分配的内存中。

3)用GetProcAddress函数来得到LoadLibraryW或LoadLibraryA函数(在kernel32.dll中)的实际地址。

4)用CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibrary函数(版本)并在参数中传入第1步分配的内存地址(DLL路径名)。这时,DLL已经被注入到远程进程的地址空间中,DLL的DllMain函数会收到DLL_PROCESS_ATTACH通知并且可以执行我们想要执行的代码。当DllMain返回的时候,远程线程会从LoadLibraryW/A调用返回BaseThreadStart函数。BaseThreadStart然后调用ExitThread,使远程线程终止。

现在远程进程中有一块内存,它是我们在第1步分配的,DLL也还在远程进程的地址空间中。为了对它们进行清理,我们需要在远程线程退出之后执行后续步骤。

5)用VirtualFreeEx来释放第1步分配的内存。

6)用GetProcAddress来得到FreeLibrary函数(在kernel32.dll中)的实际地址。

7)用CreateRemoteThread函数在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的HMODULE。

 

三、关键代码分析

        根据上面的步骤,远程线程注入DLL的关键代码主要是在”InjLib工程“的”InjLibW函数“和”EjectLibW函数"中。前者是注入,后者是清理。下面就几个重要细节展开说一下。

1,拿到目标进程的句柄

 

      // Get a handle for the target process.
      hProcess = OpenProcess(
         PROCESS_QUERY_INFORMATION |   // Required by Alpha
         PROCESS_CREATE_THREAD     |   // For CreateRemoteThread
         PROCESS_VM_OPERATION      |   // For VirtualAllocEx/VirtualFreeEx
         PROCESS_VM_WRITE,             // For WriteProcessMemory
         FALSE, dwProcessId);

        该示例是需要手动输入目标进程ID的(这个ID可以通过系统进程管理器看到)。但是,实际中,我们需要将进程ID转为进程句柄来作为Windows API的参数。

 

 

2,获取导入函数的实际地址

 

      // Get the real address of LoadLibraryW in Kernel32.dll
      PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
         GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");


        在前一篇的原理介绍中,我们已经讲解了:kernel32.dll等几个系统DLL在各个进程中的“start address"的值是相等的。它的导出函数在各个进程中的虚拟地址值也是相等的。因此,我们只需要在我们的进程中计算出”LoadLibraryW/A“的地址值,即可知道它在目标进程的地址值,而且可以直接用这个地址值赋值给函数指针,并通过指针来调用该函数。

 

        简单来说,是这样的。但是,我们要怎样来获得”LoadLibraryW/A“的地址值呢?

        对于我们自己编写的函数(非导出函数),我们可以定义一个函数指针,把目标函数直接赋值给函数指针,再把函数指针转换为long类型,即可获得该函数的地址值。也可以将函数指针转为void*进行传递。

 

typydef int (*pFn)(int, int);
pFn = FunctionA;
long lValue = (long)pFn;

        尽管”LoadLibraryW/A“在我们的程序中,我们可以像使用我们自定义的非导出函数一样直接调用,但是,我们却不能像上面一样获得它的真实地址值。我们也不能直接把”LoadLibraryW/A“作为第4个参数传给CreateRemoteThread。因为,我们忽略了一点——”LoadLibraryW/A“是导出函数。

 

        (下面这段话摘自参考链接中的博文,稍有修改)

 

        我们知道导入函数的真实地址是在DLL加载的时候获得的。加载程序从导入表取得每一个导入函数的函数名(字符串),然后在被加载到进程地址空间的DLL中查询之后,填到导入表的相应位置(IAT)的。也就是说在运行之前我们并不知道导入函数的地址(当然模块绑定过得除外)。

1)那么程序代码中是如何表示对导入函数的调用呢?

或许我们觉得应该是:CALL DWORD PTR[004020108]  ( [ ]内仅表示导入函数地址,无实际意义)。

由于程序的代码在经过编译连接之后就已经确定,而导入表的地址如00402010是在程序运行的时候获得的。所以程序在调用导入函数的时候并不能这样实现。那到底是如何实现的呢?

2)[ ]内有一个确定的地址这是毋庸置疑的,但是他的值并不是导入函数的地址,而是一个子程序的地址。该子程序被称为转换函数(thunk)。这些转换函数用来跳转到导入函数。当程序调用导入函数时,先会调用转换函数,转换函数从导入表的IAT获得导入函数的真实地址时在调用相应地址。

3)导入函数的调用形式:   

[cpp] view plaincopy

  1.          CALL  00401164 ; 转换函数的地址。  
  2.   
  3.             ...... 
  4.   
  5.          :00401164  
  6.   
  7.             ......
  8.   
  9.          CALL DWORD PTR [00402010];  调用导入函数。  

4)至此我们会明白为什么在声明一个导出函数的时候要加上_decllpec(dllimport)前缀。

原因是:编译器无法区分应用程序是对一般函数的调用还是对导入函数的调用。当我们在一个函数前加上此前缀就是告诉编译器此函数来自导入函数,编译器就会产生如上的指令。而不是CALL XXXXXXXX的形式。

所以在写一个输出函数的时候一定要在函数声明前加上修饰符:_decllpec(dllimport)。

 

        也就是说,如果我们在调用CreateRemoteThread的时候直接引用”LoadLibraryW“,该引用会被解析为我们模块的导入段中的”LoadLibraryW“转换函数的地址。如果把这个转换函数的地址作为远程线程的起始地址传入,那么天知道远程线程会执行什么代码,其结果很可能是访问违规。为了强制代码略过转换函数并直接调用”LoadLibraryW“函数,我们必须通过GetProcAddress来得到”LoadLibraryW“的确切地址。

        事实上,我们在自己的代码中显示加载某个DLL时,也需要通过GetProcAddress来获得该DLL的导出函数的真实地址。

 

3,获取远程DLL的HMODULE

 

      // Grab a new snapshot of the process
      hthSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
      if (hthSnapshot == INVALID_HANDLE_VALUE) __leave;

      // Get the HMODULE of the desired library
      MODULEENTRY32W me = { sizeof(me) };
      BOOL bFound = FALSE;
      BOOL bMoreMods = Module32FirstW(hthSnapshot, &me);
      for (; bMoreMods; bMoreMods = Module32NextW(hthSnapshot, &me)) {
         bFound = (_wcsicmp(me.szModule,  pszLibFile) == 0) || 
                  (_wcsicmp(me.szExePath, pszLibFile) == 0);
         if (bFound) break;
      }
      if (!bFound) __leave;

        参考MSDN的链接:CreateToolhelp32Snapshot 函数可以给指定进程拍一个快照(snapshot)。该快照包括这些信息:heaps、modules和threads。获得了远程进程的snapshot后,遍历该snapshot,匹配它的各个module entry的module name或path,找到目标Library的Module。

 

 

4,DLL的入口点函数

        我们用VS新建一个win32的DLL,它会自动生成如下的入口点函数

 

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}


        《Windows核心编程》第20章”DLL高级技术“中对”DLL的入口函数“做了如下说明:

 

1)一个DLL可以有一个入口函数(也可以没有)。系统会在不同的时候调用这个入口点函数。这些调用都是通知性质的,通常被DLL用来执行一些与进程或线程有关的初始化和清理工作。如果DLL不需要这些通知,那么我们可以不必在源码中实现这个入口点函数。(一般默认VS生成的代码,不去修改它)

2)在链接DLL的时候,如果链接器无法在DLL的.obj文件中找到一个名为DllMain的函数,那么它会链接C/C++运行库的DllMain函数。

3)系统通过”计数“管理某个进程中所有线程载入某个DLL的次数,线程调用LoadLibrary时,计数+1,线程执行FreeLibrary时,技术-1。只有计数为1时,才会真正执行映射,其他的仅仅增加计数,然后直接跳过。

 

        ImageWalk工程是用于注入的DLL,它的功能就是在DllMain函数的”DLL_PROCESS_ATTACH“通知分支中实现的。此外,我们也可以在该分支中再开线程,执行其他扩展代码。
 

四、源码调试中遇到的问题

1,注入系统应用程序失败

        《Windows核心编程》一书中关于”远程线程注入DLL“,需要使用源码中的两个工程来配合实现。ImageWalk工程用来生成待注入的DLL,InjLib工程用于执行注入工作。我们需要先编译生成ImageWalk.dll,再运行InjLib.exe。

        但是,我在实际调试的时候,发现:每次注入系统自带的应用程序,如:calc.exe、explorer.exe等都不能成功。而注入非系统自带的应用程序,如notepad++、feiq.exe等则可以成功。

        事实上,我最初尝试的时候,按照原书的推荐,一直用explorer.exe和calc.exe作为目标进程,每次在调CreateRemoteThread时都失败,一度怀疑:是否是我的Windows NT比原书作者用的版本新,Windows已关闭这个功能。直到一次偶然注入notepad++成功,才豁然开朗。

        此后,我一直在分析”为什么对系统自带的应用程序注入不成功,而非自带应用程序能注入成功“。最终发现,还是与我的系统版本有关。我的系统版本是:win10 X64。

        当我用”vmmap.exe“来观察notepad++、feiq.exe中kernel32.dll的”start address",我发现,它们的值相等,且都小于0x7FFEFFFF(参考我的上一篇博文);而观察calc.exe、explorer.exe的时候,发现它们俩之间的值也相等,但是这个值大于0x7FFEFFFF,比如为0x00007FFA37F60000,一看就是一个64位的值。

        至此,我终于明白,系统自带的应用程序是64位的应用程序,它的系统DLL的”start address"和我用于注入DLL的InjLib程序(32位)的值不相等,这样才导致失败。

        当我在VS中将源码从Win32模式改为X64模式,从新编译ImageWalk工程和InjLib工程,注入calc.exe、explorer.exe成功!!!

参考链接:

http://www.freebuf.com/articles/system/94693.html

http://blog.csdn.net/woshibendangao/article/details/23086205

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值