查看原文
其他

原创 | 驱动病毒那些事(一)—— 基础

李哈哈 SecIN技术平台 2022-06-18
点击上方蓝字 关注我吧


最近由于工作需要,需要分析驱动病毒样本,提出查杀方案。本人之前没有接触过Ring 0驱动层的查杀分析,将自己在学习过程中踩得坑记录下来,为想入坑驱动的小伙伴提供一点思路。
这是一个系列文章,由于本人水平有限,文章如有叙述错误,请大佬斧正,在评论区交流。
驱动病毒定义

 

内核模式驱动级病毒的特征是运行在内核驱动级、能与安全软件抗衡、可能不会对系统进行直接破坏但为别的病毒提供下载支持。内核模式驱动级病毒本身虽然不具备盗号功能,但它具备对抗安全软件及系统自带的安全工具的功能。


这类病毒在病毒产业链中起着核心作用,它一般先会对安全软件进行劫持、关闭防火墙、阻止系统升级,当成功后再下载大量其它木马病毒,进行下一阶段的攻击。


开发环境配置


工欲善其事,必先利其器。在进行驱动病毒样本分析之前,需要先配置病毒分析环境。我列了一个配置清单。
1.VS219驱动开发环境+ debugview
2.Windbg+VirtualKD双机调试环境
3.PCHunter分析工具
4.WRK源代码一份+Vscode代码阅读工具
5.InstDrv或KmdManager等驱动加载工具

这些工具和安装流程网上都有详细介绍,在此不再赘述。

如果大家在安装完成VS后,创建新项目时没有KMDF项目。安装的WDK 的文件夹下选择Vsix文件夹。




双击WDK.vsix安装即可。如果报下图错误,检查下载的 WDK版本是否与 VS 2019下载时的 Windows 10 SDK 版本对应。



出现该界面即可,大功告成。


下面给大家介绍驱动中常见的数据结构,我们只有熟悉了驱动的数据结构,无论是在IDA静态分析结构体识别,还是windbg调试dt命令查看结构体信息,才能得心应手。


重要的数据结构



1.驱动对象DRIVER_OBJECT


typedef struct _DRIVER_OBJECT { CSHORT Type; CSHORT Size; PDEVICE_OBJECT DeviceObject; //指向Driver创建的设备对象的指针。 ULONG Flags; PVOID DriverStart; //驱动对象的起始地址 ULONG DriverSize; //驱动对象的大小 PVOID DriverSection; //驱动对象结构.可以解析为_LDR_DATA_TABLE_ENTRY 是一个链表存储着下一个驱动对象 PDRIVER_EXTENSION DriverExtension; UNICODE_STRING DriverName; // //驱动对象的名字 PUNICODE_STRING HardwareDatabase; //指向一个NICODE_STRING字符串,此字符串指向的注册表路径包含了硬件配置信息。 PFAST_IO_DISPATCH FastIoDispatch; //文件驱动用到的派遣函数 PDRIVER_INITIALIZE DriverInit; //驱动程序的DriverEntry 例程入口点。 PDRIVER_STARTIO DriverStartIo; PDRIVER_UNLOAD DriverUnload; //卸载驱动的时候的回调函数。 PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]; //该成员是一个PDRIVER_DISPATCH结构体组数指针,每个指针指向一个处理IRP的派遣函数。} DRIVER_OBJECT, *PDRIVER_OBJECT;


该结构体共有15个成员,这个结构体表示的是一个驱动对象,是驱动的一个实例。该对象在DriverEntry函数中被始化,然后由内核中的I/O管理器来加载,确切的加载函数是IoCreateDevice。

我们也可以通过windbg调试,找到该结构体地址,用dt命令查看具体结构体信息。



2.设备对象DEVICE_OBJECT


typedef struct _DEVICE_OBJECT { CSHORT Type; USHORT Size; LONG ReferenceCount; //引用计数 struct _DRIVER_OBJECT *DriverObject; // 该设备所属的驱动对象 struct _DEVICE_OBJECT *NextDevice; //指向下一个设备对象的指针(如果存在) struct _DEVICE_OBJECT *AttachedDevice; struct _IRP *CurrentIrp; //当前IRP。 PIO_TIMER Timer; ULONG Flags; ULONG Characteristics; __volatile PVPB Vpb; PVOID DeviceExtension; //设备扩展结构指针,指向设备扩展对象。 DEVICE_TYPE DeviceType; CCHAR StackSize; //指定发送给该驱动的IRP中stacklocation 的大小(最小值) union { LIST_ENTRY ListEntry; WAIT_CONTEXT_BLOCK Wcb; } Queue; ULONG AlignmentRequirement; KDEVICE_QUEUE DeviceQueue;// 设备对象的队列。驱动队列中包含了与驱动对象相应的等待驱动处理的IRP。 KDPC Dpc; ULONG ActiveThreadCount; PSECURITY_DESCRIPTOR SecurityDescriptor; KEVENT DeviceLock;// 由I/O管理器创建的同步事件对象。 USHORT SectorSize; USHORT Spare1; struct _DEVOBJ_EXTENSION *DeviceObjectExtension; PVOID Reserved;} DEVICE_OBJECT, *PDEVICE_OBJECT;


该结构体保存的是设备对象指针链表,其成员包含了当前设备对象指针和下一个设备对象指针。在卸载驱动时要遍历驱动程序的所有设备对象,并用IoDeleteDevice来删除所有设备对象。

设备对象(DREVICE_OBJECT)是唯一可以接收请求的实体,任何一个请求(IRP)都是发送给某个设备对象的,一个设备对象总是属于一个驱动对象。


3. 驱动入口DriverEntry


NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, //指向驱动程序对象结构的指针,该结构表示驱动程序的 WDM 驱动程序对象。 _In_ PUNICODE_STRING RegistryPath //指向 UNICODE字符串结构的指针,该字符串结构指定注册表中驱动程序的 Parameters 项的路径。);


DriverEntry是驱动程序的入口函数,相当于C/C++程序的main函数,负责驱动初始化工作。


4.派遣函数IRP


派遣函数的原型为:
NTSTATUS Dispatch(PDEVICE_OBJECT deivce,PIRP irp);

第一个参数为请求的目标设备,第二个参数为请求的指针。


IRP类型
IRP_MJ_CREATE创建设备,CreatFile会产生此IRP
IRP_MJ_CLOSE关闭设备,CloseHandle会产生此IRP
IRP_MJ_CLEANUP清除工作,CloseHandle会产生此IRP

IRP_MJ_DEVICE_CONTROL DeviceIoControl函数会产生此IRP


IRP_MJ_PNP即插即用消息,NT式驱动不支持此种IRP,只有WDM 驱动才支持此种IRP
IRP_MJ_POWER在操作系统处理电源消息时,产生此IRP
IRP_MJ_QUERY_INFORMATION获取文件长度,GetFileSize会产生此IRP


IRP_MJ_READ读取设备内容,ReadFile会产生此IRP

IRP_MJ_SET_INFORMATION设置文件长度,GetFileSize会产生此IRP
IRP_MJ_SHUTDOWN关闭系统前会产生此IRP
IRP_MJ_SYSTEM_CONTROL系统内部产生的控制信息,类似于内核调用DeviceIoControl函数
IRP_MJ_WRITE对设备进行WriteFile时会产生此IRP


举个例子,应用层调用CreateFile触发IRP_MJ_CREATE,时, IO管理器把IRP请求传递
到设备栈中的设备对象,设备对象通过结构体中DriverObject成员找到驱动对象,驱动对象通过检查结构中MajorFunction字段,确定执行什么操作。若创建成功,则应用层会收到一个此文件对象的句柄 。

内核模块并不生成一个进程,只是填写一组回调函数让Windows调用

具体调用细节可以参考:https://wenku.baidu.com/view/117ca249336c1eb91a375d1f.html



5.双向链表LIST_ENTRY


typedef struct _LIST_ENTRY {struct _LIST_ENTRY *Flink;struct _LIST_ENTRY *Blink;}LIST_ENTRY,*PLIST_ENTRY

其中的成员Flink为指向下一个节点的指针,成员Blink为指向前一个节点的指针


6.字符串UNICODE_STRING


typedef struct _UNICODE_STRING { USHORT Length; //字符串的所占的字节数 USHORT MaximumLength;// 字符串所能占的最大字节数字符串的指针 PWCH Buffer; //指向宽字符串的指针} UNICODE_STRING;


这里着重说明一下UNICODE_STRING初始化。常用的初始化方法有三种。

1.常量内存,调用RtlInitUnicodeString 函数进行初始化。
UNICODE_STRING v1;RtlInitUnicodeString(&v1, L"HelloWorld"); DbgPrint("%wZ\r\n", &v1);

2.手工赋值


UNICODE_STRING v1;WCHAR Data[] = L"HelloWorld";v1.Buffer = Data;v1.Length = wcslen(Data)*sizeof(WCHAR);v1.MaximumLength = (wcslen(Data)+1)*sizeof(WCHAR); DbgPrint("%wZ\r\n", &v1);
3.动态内存ExAllocatePool函数动态分配。
UNICODE_STRING v1; WCHAR Data[] = L"HelloWorld"; v1.Length = wcslen(Data) * sizeof(WCHAR); v1.MaximumLength = (wcslen(Data) + 1) * sizeof(WCHAR); v1.Buffer = ExAllocatePool(PagedPool, v1.MaximumLength);

这里有一点需要注意当应用程序与驱动通信时,一般应用程序传入的字符串为ANSI,所以在驱动中应先定义ANSI_STRING,然后再使用RtlAnsiStringToUnicodeString 将其转换成UNICODE_STRING,作为后用,否则会有蓝屏的危险。


7. 设备链接名称与设备名称关联 IoCreateSymbolicLink


NTSTATUS IoCreateSymbolicLink( PUNICODE_STRING SymbolicLinkName,//符号链接名用于ring3通信 PUNICODE_STRING DeviceName //设备对象的名字);


当调用IoCreateDevice创建设备对象后,我们就能通过DeviceName来访问这个设备,但是这个设备名称只能在内核层使用。当时很多时候我们需要与用户层进行交互,这时就需要设置符号链接,符号链接名可以被用户模式下的应用程序识别。
需要注意的是设备对象名称需要用UNICODE字符串指定,并且字符串必须是L"\\Device\\ [设备名]”这种形式。
驱动中符号链接的命名规则为L"\\??\\ [设备名]”或L"\\DosDevices\\[设备名]"
在应用层中用L”\\.\[设备名]”来标识符号链接名(""是C/C+中转义符, "\\.\"相当于\.)

面用一个实例来向大家展示:

Ring 0:
先用IoCreateDevice函数创建设备对象,再用IoCreateSymbolicLink将符号链接名与设备对象名称关联

#define DRV_DEVICEL"\\Device\\IOname"//设备名称#define DRV_SYSLNKL"\\??\\IOname "//符号链接UNICODE_STRING devName;RtlInitUnicodeString(&devName, DRV_DEVICE);status = IoCreateDevice(pDriverObject, sizeof(DEVICE_EXTENSION),&devName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDevObj); //创建设备UNICODE_STRING symLinkName;RtlInitUnicodeString(&symLinkName, DRV_SYSLNK);status = IoCreateSymbolicLink(&symLinkName, &devName);//设备链接名与设备名关联

Ring 3:
应用层通过符号链接名调用CreateFile函数获取到设备句柄DeviceHandle。再调用DeviceIoControl函数就可以通过这个DeviceHandle发送控制码了。


#define DEVICE_LINK_NAME L"\\\\.\\IOname "HANDLE hDevice = CreateFile( DEVICE_LINK_NAME, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);


驱动调试


1.当我们有驱动PDB符号表时,可以使用命令.sympath+pdb符号路径来设置符号路径。当我们有源代码或者调试自己编写的驱动时,还可以设置源代码调试,命令为.srcpath+源代码路径。

设置符号路径后,可以直接使用命令bu 驱动名称!DriverEntry,windbg就会在驱动入口断下。

源代码调试效果如下:


当我们对自己开发的驱动进行测试时,需要进入测试模式才能正常运行。给大家安利一个软件dseo13b,可以方便切换模式。



2.当没有符号文件时,通常情况下,我们是无法获得驱动病毒符号的,这时我通常IopLoadDriver函数下断点。



uf nt!IopLoadDriver

64位下:


32位下:



直接bp 地址即可。


结语


内核模块并非和普通应用程序一样作为一个进程执行,而是运行在内核空间,成为操作系统的一个模块,最终被所有需要该模块提供功能的应用程序(也可能被操作系统本身)调用。

这里有一点需要注意,也是我学习时候的一个误区,认为所有内核代码都运行在系统进程内。事实上只有DriverEntry函数被调用时,一般位于系统进程中,Windows一般都用系统进程来加载内核模块,并不是说内核模块始终运行在System进程中。
参考链接

1.《Windows内核安全与驱动开发》
2.
https://www.cnblogs.com/lsh123/p/7354573.html
3. https://bbs.pediy.com/thread-228575.htm
4. https://wenku.baidu.com/view/117ca249336c1eb91a375d1f.html
5. https://www.cnblogs.com/zudn/archive/2010/12/30/1921457.html
6. https://www.cnblogs.com/hgy413/p/3693361.html
7. https://www.cnblogs.com/lsh123/p/7354573.html
8. https://www.cnblogs.com/zudn/archive/2010/12/30/1921457.html


往期推荐

一次金融行业的红蓝对抗总结

浅谈Fastjson Waf Bypass

HFish初版审计学习



你要的分享、在看与点赞都在这儿~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存