【安全技术】关于几种dll注入方式的学习

何为dll注入

DLL注入技术,一般来讲是向一个正在运行的进程插入/注入代码的过程。我们注入的代码以动态链接库(DLL)的形式存在。DLL文件在运行时将按需加载(类似于UNIX系统中的共享库(share object,扩展名为.so))。然而实际上,我们可以以其他的多种形式注入代码(正如恶意软件中所常见的,任意PE文件,shellcode代码/程序集等)。

全局钩子注入

在Windows大部分应用都是基于消息机制,他们都拥有一个消息过程函数,根据不同消息完成不同功能,windows通过钩子机制来截获和监视系统中的这些消息。一般钩子分局部钩子与全局钩子,局部钩子一般用于某个线程,而全局钩子一般通过dll文件实现相应的钩子函数。

核心函数

SetWindowsHookEx

通过设定钩子类型与回调函数的地址,将定义的钩子函数安装到挂钩链中。如果函数成功返回钩子的句柄,如果函数失败,则返回NULL

实现原理

由上述介绍可以知道如果创建的是全局钩子,那么钩子函数必须在一个DLL中。这是因为进程的地址空间是独立的,发生对应事件的进程不能调用其他进程地址空间的钩子函数。如果钩子函数的实现代码在DLL中,则在对应事件发生时,系统会把这个DLL加较到发生事体的进程地址空间中,使它能够调用钩子函数进行处理。

在操作系统中安装全局钩子后,只要进程接收到可以发出钩子的消息,全局钩子的DLL文件就会由操作系统自动或强行地加载到该进程中。因此,设置全局钩子可以达到DLL注入的目的。创建一个全局钩子后,在对应事件发生的时候,系统就会把 DLL加载到发生事件的进程中,这样,便实现了DLL注入。

为了能够让DLL注入到所有的进程中,程序设置WH_GETMESSAGE消息的全局钩子。因为WH_GETMESSAGE类型的钩子会监视消息队列,并且 Windows系统是基于消息驱动的,所以所有进程都会有自己的一个消息队列,都会加载 WH_GETMESSAGE类型的全局钩子DLL。

那么设置WH_GETMESSAGE就可以通过以下代码实现,记得加上判断是否设置成功


这里第二个参数是回调函数,那么我们还需要写一个回调函数的实现,这里就需要用到CallNextHookEx这个api,主要是第一个参数,这里传入钩子的句柄的话,就会把当前钩子传递给下一个钩子,若参数传入0则对钩子进行拦截


既然我们写入了钩子,如果不使用的情况下就需要将钩子卸载掉,那么这里使用到UnhookWindowsHookEx这个api来卸载钩子


既然我们使用到了SetWindowsHookEx这个api,就需要进行进程间的通信,进程通信的方法有很多,比如自定义消息、管道、dll共享节、共享内存等等,这里就用共享内存来实现进程通信



实现过程

首先新建一个dll

在pch.h头文件里面声明这几个我们定义的函数都是裸函数,由我们自己平衡堆栈


然后在pch.cpp里面写入三个函数并创建共享内存


然后再在dllmain.cpp设置DLL_PROCESS_ATTACH,然后编译生成Golbal.dll


再创建一个控制台项目

使用LoadLibrabryW加载dll,生成GolbalInjectDll.cpp文件



执行即可注入GolbalDll.dll

远程线程注入

远程线程函数顾名思义,指一个进程在另一个进程中创建线程。

核心函数

CreateRemoteThread

lpStartAddress:A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the starting address of the thread in the remote process. The function must exist in the remote process. For more information, see ThreadProc).

lpParameter:A pointer to a variable to be passed to the thread function.

lpStartAddress即线程函数,使用LoadLibrary的地址作为线程函数地址;lpParameter为线程函数参数,使用dll路径作为参数

VirtualAllocEx

是在指定进程的虚拟空间保留或提交内存区域,除非指定MEM_RESET参数,否则将该内存区域置0。



hProcess:申请内存所在的进程句柄

lpAddress:保留页面的内存地址;一般用NULL自动分配 。

dwSize:欲分配的内存大小,字节单位;注意实际分 配的内存大小是页内存大小的整数倍。

flAllocationType

可取下列值:

MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储

MEM_PHYSICAL :分配物理内存(仅用于地址窗口扩展内存)

MEM_RESERVE:保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用VirtualAlloc()而被占用

MEM_RESET :指明在内存中由参数lpAddress和dwSize指定的数据无效

MEM_TOP_DOWN:在尽可能高的地址上分配内存(Windows 98忽略此标志)

MEM_WRITE_WATCH:必须与MEM_RESERVE一起指定,使系统跟踪那些被写入分配区域的页面(仅针对Windows 98)

flProtect

可取下列值:

PAGE_READONLY: 该区域为只读。如果应用程序试图访问区域中的页的时候,将会被拒绝访

PAGE_READWRITE 区域可被应用程序读写

PAGE_EXECUTE: 区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。

PAGE_EXECUTE_READ :区域包含可执行代码,应用程序可以读该区域。

PAGE_EXECUTE_READWRITE: 区域包含可执行代码,应用程序可以读写该区域。

PAGE_GUARD: 区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限

PAGE_NOACCESS: 任何访问该区域的操作将被拒绝

PAGE_NOCACHE: RAM中的页映射到该区域时将不会被微处理器缓存(cached)

注:PAGE_GUARD和PAGE_NOCHACHE标志可以和其他标志合并使用以进一步指定页的特征。PAGE_GUARD标志指定了一个防护页(guard page),即当一个页被提交时会因第一次被访问而产生一个one-shot异常,接着取得指定的访问权限。PAGE_NOCACHE防止当它映射到虚拟页的时候被微处理器缓存。这个标志方便设备驱动使用直接内存访问方式(DMA)来共享内存块。

WriteProcessMemory

此函数能写入某一进程的内存区域(直接写入会出Access Violation错误),故需此函数入口区必须可以访问,否则操作将失败。



实现原理

使用CreateRemoteThread这个API,首先使用CreateToolhelp32Snapshot拍摄快照获取pid,然后使用Openprocess打开进程,使用VirtualAllocEx

远程申请空间,使用WriteProcessMemory写入数据,再用GetProcAddress获取LoadLibraryW的地址(由于Windows引入了基址随机化ASLR安全机制,所以导致每次开机启动时系统DLL加载基址都不一样,有些系统dll(kernel,ntdll)的加载地址,允许每次启动基址可以改变,但是启动之后必须固定,也就是说两个不同进程在相互的虚拟内存中,这样的系统dll地址总是一样的),在注入进程中创建线程(CreateRemoteThread)

实现过程

首先生成一个dll文件,实现简单的弹窗即可


我们要想进行远程线程注入,那么就需要得到进程的pid,这里使用到的是CreateToolhelp32Snapshot这个api拍摄快照来进行获取,注意我这里定义了#include "tchar.h",所有函数都是使用的宽字符


首先使用OpenProcess打开进程


然后使用VirtualAllocEx远程申请空间


然后写入内存,使用WriteProcessMemory


然后创建线程并等待线程函数结束,这里WaitForSingleObject的第二个参数要设置为-1才能够一直等待


综上完整代码如下





然后这里生成一个test.exe来做测试

编译并运行,实现效果如下

突破session 0的远程线程注入

首先提一提session0的概念:

Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3,RING0只给操作系统用,RING3谁都能用。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。

ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之…… 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。

RING设计的初衷是将系统权限与程序分离出来,使之能够让OS更好的管理当前系统资源,也使得系统更加稳定。举个RING权限的最简单的例子:一个停止响应的应用程式,它运行在比RING0更低的指令环上,你不必大费周章的想着如何使系统回复运作,这期间,只需要启动任务管理器便能轻松终止它,因为它运行在比程式更低的RING0指令环中,拥有更高的权限,可以直接影响到RING0以上运行的程序,当然有利就有弊,RING保证了系统稳定运行的同时,也产生了一些十分麻烦的问题。比如一些OS虚拟化技术,在处理RING指令环时便遇到了麻烦,系统是运行在RING0指令环上的,但是虚拟的OS毕竟也是一个系统,也需要与系统相匹配的权限。而RING0不允许出现多个OS同时运行在上面,最早的解决办法便是使用虚拟机,把OS当成一个程序来运行。

核心函数

ZwCreateThreadEx

注意一下这个地方ZwCreateThreadEx这个函数在32位和64位中的定义不同

在32位的情况下


在64位的情况下


这里因为我们要进到session 0那么就势必要到system权限,所以这里还有几个提权需要用到的函数

OpenProcessToken

LookupPrivilegeValueA

AdjustTokenPrivileges


实现原理

ZwCreateThreadEx比 CreateRemoteThread函数更为底层,CreateRemoteThread函数最终是通过调用ZwCreateThreadEx函数实现远线程创建的。

通过调用CreateRemoteThread函数创建远线程的方式在内核6.0(Windows VISTA、7、8等)以前是完全没有问题的,但是在内核6.0 以后引入了会话隔离机制。它在创建一个进程之后并不立即运行,而是先挂起进程,在查看要运行的进程所在的会话层之后再决定是否恢复进程运行。

在Windows XP、Windows Server 2003,以及更老版本的Windows操作系统中,服务和应用程序使用相同的会话(Session)运行,而这个会话是由第一个登录到控制台的用户启动的。该会话就叫做Session 0,如下图所示,在Windows Vista之前,Session 0不仅包含服务,也包含标准用户应用程序。

将服务和用户应用程序一起在Session 0中运行会导致安全风险,因为服务会使用提升后的权限运行,而用户应用程序使用用户特权(大部分都是非管理员用户)运行,这会使得恶意软件以某个服务为攻击目标,通过“劫持”该服务,达到提升自己权限级别的目的。

从Windows Vista开始,只有服务可以托管到Session 0中,用户应用程序和服务之间会被隔离,并需要运行在用户登录到系统时创建的后续会话中。例如第一个登录的用户创建 Session 1,第二个登录的用户创建Session 2,以此类推,如下图所示。

使用CreateRemoteThread注入失败DLL失败的关键在第七个参数CreateThreadFlags, 他会导致线程创建完成后一直挂起无法恢复进程运行,导致注入失败。而想要注册成功,把该参数的值改为0即可。

实现过程

在win10系统下如果我们要注入系统权限的exe,就需要使用到debug调试权限,所以先写一个提权函数。


在进程注入dll的过程中,是不能够使用MessageBox的,系统程序不能够显示程序的窗体,所以这里编写一个ShowError函数来获取错误码


首先打开进程获取句柄,使用到OpenProcess


然后是在注入的进程申请内存地址,使用到VirtualAllocEx


再使用WriteProcessMemory写入内存

    

加载ntdll,获取LoadLibraryA函数地址


获取ZwCreateThreadEx函数地址


使用 ZwCreateThreadEx创建远线程, 实现 DLL 注入


这里还有一点需要注意的是ZwCreateThreadEx在 ntdll.dll 中并没有声明,所以我们需要使用 GetProcAddress从 ntdll.dll中获取该函数的导出地址

这里加上ZwCreateThreadEx的定义,因为64位、32位结构不同,所以都需要进行定义


完整代码如下





因为在dll注入的过程中是看不到messagebox的,所以这里我选择cs注入进行测试,若注入成功即可上线

首先生成一个32位的dll文件,这里跟位数有关,我选择注入的是32位的进程,所以这里我选择生成32位的dll

得到路径

这里我选择的是有道云笔记进行注入,查看一下pid

然后把我们函数的pid改为有道云的pid

实现效果如下所示


APC注入

APC,全称为Asynchronous Procedure Call,即异步过程调用,是指函数在特定线程中被异步执行,在操作系统中,APC是一种并发机制。

这里去看一下msdn中异步过程调用的解释如下

首先第一个函数

QueueUserApc: 函数作用,添加制定的异步函数调用(回调函数)到执行的线程的APC队列中

APCproc:   函数作用: 回调函数的写法.

往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候,就会执行APC函数,APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC。这里介绍一下应用程序的APC,APC是往线程中插入一个回调函数,但是用的APC调用这个回调函数是有条件的,如msdn所示


核心函数

QueueUserAPC

QueueUserAPC 函数的第一个参数表示执行函数的地址,当开始执行该APC的时候,程序会跳转到该函数地址处来执行。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含THREAD_SET_CONTEXT 访问权限。第三个参数表示传递给执行函数的参数,与远线程注入类似,如果QueueUserAPC 的第一个参数为LoadLibraryA,第三个参数设置的是dll路径即可完成dll注入。

实现原理

在 Windows系统中,每个线程都会维护一个线程 APC队列,通过QucueUserAPC把一个APC 函数添加到指定线程的APC队列中。每个线程都有自己的APC队列,这个 APC队列记录了要求线程执行的一些APC函数。Windows系统会发出一个软中断去执行这些APC 函数,对于用户模式下的APC 队列,当线程处在可警告状态时才会执行这些APC 函数。一个线程在内部使用SignalObjectAndWait 、 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函数把自己挂起时就是进入可警告状态,此时便会执行APC队列函数。

通俗点来概括过程可分为以下几步:

1)当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断(或者是Messagebox弹窗的时候不点OK的时候也能注入)。

2)当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。

3)利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的。

但是想要使用apc注入也有以下两点条件:

1.必须是多线程环境下

2.注入的程序必须会调用那些同步对象

每一个进程的每一个线程都有自己的APC队列,我们可以使用QueueUserAPC函数把一个APC函数压入APC队列中。当处于用户模式的APC被压入到线程APC队列后,线程并不会立刻执行压入的APC函数,而是要等到线程处于可通知状态(alertable)才会执行,即只有当一个线程内部调用SleepEx等上面说到的几个特定函数将自己处于挂起状态时,才会执行APC队列函数,执行顺序与普通队列相同,先进先出(FIFO),在整个执行过程中,线程并无任何异常举动,不容易被察觉,但缺点是对于单线程程序一般不存在挂起状态,所以APC注入对于这类程序没有明显效果。

实现过程

这里的常规思路是编写一个根据进程名获取pid的函数,然后根据PID获取所有的线程ID,这里我就将两个函数集合在一起,通过自己输入PID来获取指定进程的线程并写入数组



然后是apc注入的主函数,首先使用VirtualAllocEx远程申请内存


然后使用WriteProcessMemory把dll路径写入内存


再获取LoadLibraryA的地址


便利线程并插入APC,这里定义一个fail并进行判断,如果QueueUserAPC返回的值为NULL则线程遍历失败,fail的值就+1


然后在到主函数,定义dll地址


使用OpenProcess打开句柄


调用之前写好的APCInject函数实现APC注入



完整代码如下







之前说过我没有使用进程名 -> pid的方式,而是直接采用手动输入的方式,通过cin >> ulProcessID将接收到的参数赋给ulProcessID

这里可以选择写一个MessageBox的dll,这里我直接用的是cs的dll,演示效果如下所示

最后

关注私我获取【网络安全学习资料·攻略

你可能感兴趣的:(【安全技术】关于几种dll注入方式的学习)