驱动病毒那些事(一)----基础

  • 驱动病毒那些事(一)----基础已关闭评论
  • 46 views
  • A+

最近由于工作需要,需要分析驱动病毒样本,提出查杀方案。本人之前没有接触过Ring 0驱动层的查杀分析,将自己在学习过程中踩得坑记录下来,为想入坑驱动的小伙伴提供一点思路。

这是一个系列文章,由于本人水平有限,文章如有叙述错误,请大佬斧正,在评论区交流。

驱动病毒定义

内核模式驱动级病毒的特征是运行在内核驱动级、能与安全软件抗衡、可能不会对系统进行直接破坏但为别的病毒提供下载支持。内核模式驱动级病毒本身虽然不具备盗号功能,但它具备对抗安全软件及系统自带的安全工具的功能。这类病毒在病毒产业链中起着核心作用,它一般先会对安全软件进行劫持、关闭防火墙、阻止系统升级,当成功后再下载大量其它木马病毒,进行下一阶段的攻击。

开发环境配置

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

这些工具和安装流程网上都有详细介绍,在此不再赘述。
如果大家在安装完成VS后,创建新项目时没有KMDF项目。安装的WDK 的文件夹下选择Vsix文件夹。

1.png

2.png

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

3.png

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

12.png

下面给大家介绍驱动中常见的数据结构,我们只有熟悉了驱动的数据结构,无论是在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命令查看具体结构体信息。

4.png

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.png

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("%wZrn", &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("%wZrn", &v1);

  1. 动态内存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"DeviceIOname"//设备名称

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就会在驱动入口断下。

6.png

源代码调试效果如下:

7.png

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

8.png

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

9.png

uf nt!IopLoadDriver
64位下:

10.png

32位下:

11.png

直接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

相关推荐: 驱动病毒那些事(完结)----劫持

本篇作为驱动病毒分析系列的最后一篇,主要给大家展示一下驱动病毒常见的劫持手法。 对象劫持 驱动病毒经常通过对象劫持来把自己伪装成为系统正常的驱动文件,增强隐蔽性。 当系统加载驱动时,会为驱动构建一下_LDR_DATA_TABLE_ENTRY结构体,DRIVER…