内核-④驱动开发

1. 内核编程基础

1.1 环境搭建

  • windows系统版本,visual studio版本,WDK版本需要一致,建议在虚拟机重新安装一套

1.2 驱动调试

  • 符号文件,系统符号文件需要联网下载,需要FQ,调试自己的驱动程序需要在符号文件路径中加上自己写的驱动程序的PDB符号文件路径

1.3 内核编程

  • 内核编程容易蓝屏,建议使用异常处理机制
    __try{
    	//可能出错的代码
    }
    __except(filter_value) {
    	//出错时要执行的代码
    }
    //出现异常时,可根据filter_value的值来决定程序该如果执行,当filter_value的值为:
    //EXCEPTION_EXECUTE_HANDLER(1),代码进入except块
    //EXCEPTION_CONTINUE_SEARCH(0),不处理异常,由上一层调用函数处理
    //EXCEPTION_CONTINUE_EXECUTION(-1),回去继续执行错误处的代码
  • 内核API使用
    • <1> 在应用层编程我们可以使用WINDOWS提供的各种API函数,只要导入头文件<windows.h>就可以了,但是在内核编程的时候,我们不能像在Ring3那样直接使用。微软为内核程序提供了专用的API,只要在程序中包含相应的头文件就可以使用了,如:#include <ntddk.h> (假设你已经正确安装了WDK)
    • <2> 在应用层编程的时候,我们通过MSDN来了解函数的详细信息,在内核编程的时候,要使用WDK自己的帮助文档。
    • <3>也可以在线查询API
  • 未导出函数使用
    • WDK说明文档中只包含了内核模块导出的函数,对于未导出的函数,则不能直接使用。
    •  如果要使用未导出的函数,只要自己定义一个函数指针,并且为函数指针提供正确的函数地址就可以使用了。有两种办法都可以获取为导出的函数地址:
      • <1> 特征码搜索
      • <2> 解析内核PDB文件
  • 基本数据类型
    //<1> 在内核编程的时候,强烈建议大家遵守WDK的编码习惯,不要这样写:
    unsigned long length;    
    
    //<2> 习惯使用WDK自己的类型:
    ULONG(unsigned long)		PULONG(unsigned long *)
    UCHAR(unsigned char)		PUCHAR(unsigned char *)
    UINT(unsigned int)			PUNIT(unsigned int *)	
    VOID(void)					PVOID(void *)
  • 返回值
    //大部分内核函数的返回值都是NTSTATUS类型,如:
    	NTSTATUS PsCreateSystemThread();    
    	NTSTATUS ZwOpenProcess();
    	NTSTATUS ZwOpenEvent();
    
    //这个值能说明函数执行的结果,比如:
    
    	STATUS_SUCCESS				//0x00000000	成功		
    	STATUS_INVALID_PARAMETER	//0xC000000D	参数无效	
    	STATUS_BUFFER_OVERFLOW		//0x80000005	缓冲区长度不够	
    //当你调用的内核函数,如果返回的结果不是STATUS_SUCCESS,就说明函数执行中遇到了问题,
            //具体是什么问题,可以在ntstatus.h文件中查看,因为上面这些值都是在这个头文件中定义的。
  • 内核字符串种类
    //CHAR(char)/WCHAR(wchar_t)/ANSI_STRING/UNICODE_STRING
    //ANSI_STRING字符串:
    typedef struct _STRING
    {
        USHORT Length;
        USHORT MaximumLength;
        PCHAR Buffer;
    }STRING;
    //UNICODE_STRING字符串:
    typedef struct _UNICODE_STRING
    {
        USHORT Length;
        USHORT MaxmumLength;
        PWSTR Buffer;
    } UNICODE_STRING;
  • 常用内核函数
  • 最简单的驱动程序

1.4 DRIVER_OBJECT结构体

2. 内核空间和内核模块

2.0 内核模块

<1> 硬件种类繁多,不可能做一个兼容所有硬件的内核,所以,微软提供规定的接口格式,让硬件驱动人员安装规定的格式编写“驱动程序” 。
<2> 这些驱动程序每一个都是一个模块,称为“内核模块”,都可以加载到内核中,都遵守PE结构。但本质上讲,任意一个.sys文件与内核文件没有区别。

2.1 sys文件

  • .sys驱动文件是PE格式文件,加载一个.sys文件,其实就是在高2GB加载了一个模块

2.2 3环TEB查看所有模块

2.3 查看所有0环的模块

通过自己的内核对象,得到自己的内核对象地址
通过内核对象地址,找到DriverSection,这是一个指针,指向_LDR_DATA_TABLE_ENTRY,第一个成员就是自己的内核对象
_LDR_DATA_TABLE_ENTRY是一个链表,该链表将所有的内核对象链接在一起了

2.4 未导出函数

  • 1.内存遍历:找到模块基址,按照特征码搜索这个函数
    • 提取特征码:全局地址不可以作为特征码,若加载基址不一样,它将被重定位表修复,到时候就不是这个值了
    • 这些可以作为提取的特征码,但是这样也并不保险,最保险的就是:特征码  偏移  特征码  偏移  特征码
      • 56 64 a1 24  01  ??  ??  8b  ??  ??  3b  ??  ??  75  等,按照这样的样式提取特征码
  • 2.若某个导出函数调用了这个未导出函数,在这个导出函数中就会有调用的这个函数的地址 
  • 3.根据模块基址与目标函数的偏移,用基址+函数偏移

2.5 练习

3. 0环与3环的常规通讯

3.1 设备对象

  • 在开发窗口程序的时候,消息被封装成一个结构体:MSG,在内核开发时,消息被封装成另外一个结构体:IRP(I/O Request Package)。
  • 在窗口程序中,能够接收消息的只能是窗口对象。在内核中,能够接收IRP消息的只能是设备对象。

3.2 创建设备

//创建设备名称
UNICODE_STRING Devicename;
//设备对象的名字,系统会根据设备名称的\\Device字段将该设备挂到内核的一个树中,不同字段名称挂的地方不一样
RtlInitUnicodeString(&Devicename,L"\\Device\\MyDevice");
//创建设备
IoCreateDevice(
	pDriver,				//当前设备所属的驱动对象
	0,
	&Devicename,			//设备对象的名称
	FILE_DEVICE_UNKNOWN,	//类型未确定(也可以是鼠标、键盘)
	FILE_DEVICE_SECURE_OPEN,
	FALSE,
	&pDeviceObj				//设备对象指针PDEVICE_ OBJECT类型
	);

3.3 设置设备交互数据的方式

//缓冲区方式读写
pDeviceObj->Flags |= DO_BUFFERED_IO;
缓冲区方式读写(DO_BUFFERED_IO) :操作系统将应用程序提供缓冲区的数据复制到内核模式下的地址中(在高2GB使用数据)。
直接方式读写(DO_DIRECT_IO)  :操作系统会将用户模式下的缓冲区锁住。然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。相当于在内核多了一个线性地址,指向同一个物理页。缺点就是要单独占用物理页面,物理页被锁住。
其他方式读写(在调用IoCreateDevice创建设备后对pDevObj->Flags即不设置DO_BUFFERED_IO也不设置DO_DIRECT_IO此时就是其他方式)  :在使用其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中,直接操作应用程序的缓冲区地址是很危险的,一旦进程切换,地址将会错误。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。  

3.4 创建符号链接

  • 创建符号链接名称字符串
RtlInitUnicodeString(&SymbolicLinkName,L"\??\MyTestDriver");
      • \\??\\是符号链接
      • 在0环,是以\??\作为开始的标志,3环是\\.\, 转义后就是\\\\.\\
      • 因此,在0环找设备的时候: \\??\\MyTestDriver, 3环: \\\\.\\MyTestDriver
  • 创建符号链接
IoCreateSymbolicLink(&SymbolicL inkName,&Devicename);
    • 参数1:符号链接名,参数2:设备名
  • 特别说明:
    • 1、设备名称的作用是给内核对象用的,如果要在Ring3访问, 必须要有符号链接其实就是一个别名,没有这个别名,在Ring3不可见。
    • 2、内核模式下,符号链接是以"\??\"开头的,如C盘就是"??\C:"
    • 3、而在用户模式下,则是以"\\.\" 开头的,如C盘就是"\\.\C:"

3.5 设置派遣函数和卸载函数

3.5.1 IRP与派遣函数

IRP的类型
  • 当应用层通过CreateFileReadFile,WriteFile,CloseHandle等函数打开、从设备读取数据、向设备写入数据、关闭设备的时候,会使操作系统产生出IRP_MJ_CREATEIRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_CLOSE等不同的IRP消息类型。
  • 其他类型的IRP
  • IRP类型

    来源

    IRP_MJ_DEVICE_CONTROL

    3环调用DeviceIoControl函数会产生此IRP,主要用于3环与0环进行信息交互

    IRP_MJ_POWER

    在操作系统处理电源消息时,产生次IRP

    IRP_MJ_SHUTDOWN

    关闭系统前会产生此IRP

3.5.2 派遣函数注册

派遣函数注册位置

kd> dt _DRIVER_OBJECT
nt!_DRIVER_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT
   +0x008 Flags            : Uint4B
   +0x00c DriverStart      : Ptr32 Void
   +0x010 DriverSize       : Uint4B
....
   +0x030 DriverStartIo    : Ptr32     void 
   +0x034 DriverUnload     : Ptr32     void 		
   //派遣函数,每一个IRP消息类型对应一个编号,例如IRP_MJ_CREATE对应的就是0,那么CreateFile的派遣函数就是在MajorFunction[0]
   +0x038 MajorFunction    : [28] Ptr32     long 	
  • MajorFunction是一个函数数组,每一个IPR消息类型就是一个下标,对应这个数组的回调函数
  • 如IRP_MJ_CREATE,它的值是0,对应的就是MajorFunction的第0个函数
  • 因此,派遣函数的注册方法就是 pDriverObject->MajorFunction[IRP_MJ_CREATE]     = 派遣函数1; 
  • 注册派遣函数:
//注册派遣函数
NTSTATUS DriverEntry( 。。。。)  
{  
    //设置卸载函数   
    pDriverObject->DriverUnload = 卸载函数;  
    //设置派遣函数   
    pDriverObject->MajorFunction[IRP_MJ_CREATE] 			= 派遣函数1;  
    pDriverObject->MajorFunction[IRP_MJ_CLOSE] 				= 派遣函数2;  
    pDriverObject->MajorFunction[IRP_MJ_WRITE] 				= 派遣函数3;  
    pDriverObject->MajorFunction[IRP_MJ_READ] 				= 派遣函数4;  
    pDriverObject->MajorFunction[IRP_MJ_CLEANUP] 			= 派遣函数5;  
    pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] 	= 派遣函数6;  
    pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] 	= 派遣函数7;  
    pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] 			= 派遣函数8;  
    pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] 	= 派遣函数9;
} 

IRP_MJ_MAXIMUM_FUNCTION   派遣函数的最大值

3.5.3 派遣函数的格式 

//参数1设备对象指针;参数2IRP指针
NTSTATUS MyDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
	//处理自己的业务...

	//设置返回状态
	pIrp->IoStatus.Status = STATUS_SUCCESS;	//  getlasterror()得到的就是这个值
	pIrp->IoStatus.Information = 0;		//  返回给3环多少字节数据 不返回数据填0
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return STATUS_SUCCESS;
} 
编写派遣函数
// 参数pIrp:I / O 管理器在接受到应用层的设备读写请求后, 将请求封装为一个IRP请求(包含IRP 头部和IO_STACK_LOCATION)
NTSTATUS IrpDeviceContrlProc(PDEVICE_OBJECT pdriver, PIRP pIrp){
	NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
	PIO_STACK_LOCATION pIrpStack;//定义一个指向IO_STACK_LOCATION结构体的指针
	ULONG uIoControCode;
	PVOID pIoBuffer;
	ULONG uInLength;
	ULONG uOutLength;
	ULONG uRead;
	ULONG uWrite;
	//获取IRP数据
	pIrpStack = IoGetCurrentIrpStackLocation(pIrp);//根据从ring3发来的
	//获取缓冲区地址
	pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
	//ring 3发送数据的长度
	uInLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
	//ring 0 发送数据的长度
	uOutLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
	//获取操作码
	uIoControCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
	switch (uIoControCode)
	{
	case OPER1:
	{
		DbgPrint("IrpDeviceContrlProc -> OPER1 ...\n");
		pIrp->IoStatus.Information = 0;
		status = STATUS_SUCCESS;
		break;
	}
	case OPER2:
	{
		DbgPrint("IrpDeviceContrlProc -> OPER2 接受字节数:%d  \n", uInLength);
		DbgPrint("IrpDeviceContrlProc -> OPER2 接受字节数:%d  \n", uOutLength);

		//Read From Buffer
		memcpy(&uRead, pIoBuffer, 4);
		DbgPrint("IrpDeviceContrlProc -> OPER2 ...%x  \n", uRead);
		//Write To Buffer
		memcpy(pIoBuffer, &uWrite, 4);
		//set Status
		pIrp->IoStatus.Information = 2;
		status = STATUS_SUCCESS;
		break;
	}
	}
	//设置临时变量的值
	uRead = 0;
	uWrite = 0x12345678;
	//设置返回状态
	pIrp->IoStatus.Information = status;
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
	DbgPrint("DispatchDeviceControl ...  \n");
	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0;
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return STATUS_SUCCESS;
}

3.5.4 3环与0环交互

  • 3环调用DeviceIoControl函数时,会产生值为IRP_MJ_DEVICE_CONTROL的IRP
  • DeviceControl函数
    //hDevice:设备句柄
    //dwIoCode :操作码
    //nBuff:输入缓冲区地址(向0环传入的缓冲区地址)
    //InBuffLen :输入缓冲区长度
    //QutBuff:从0环返回的缓冲区地址
    //0utBuffLen :从0环返回的缓冲区长度
    //dw:返回长度
    //NULL:指向OVERLAPPED,此处设置为空
    DeviceIoControl(hDevice, dwIoCode nBuff, InBuffLen ,QutBuff,0utBuffLen ,&dw,NULL);
    • dwIoCode
      //使用windows的宏CTL_CODE生成的,dwIoCode  = CTL_CODE(...);
      #define OPER1 CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
      //参数1:FILE_DEVICE_UNKNOWN,设备类型(鼠标?键盘?还是什么,这里写的是未知)
      //参数2:0x800,0~0x7FFF保留,不能用,只能用8000~FFFF
      //参数3:METHOD_BUFFERED与设备交互的方式相对应,缓冲区选择METHOD_BUFFERED,直接IO选择另外两个...
      //参数4:FILE_ANY_ACCESS,设备权限
      #define OPER2 CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)
    • 3环0环都需要定义相同的dwIoCode,在3环中,通过pIRP获取可以获取到dwIoCode的数值,再判断获取的数值与宏定义的dwIoCode是否一致,就可以处理相关操作了,详情可以查看3.5.3派遣函数的格式 
  • 流程:
    • ①3环调用CreateFile打开设备
      • 0环会执行IRP_MJ_CREATE对应的派遣函数
    • ②3环调用DeviceControl函数
      • 0环会执行IRP_MJ_DEVICE_CONTROL对应的派遣函数
    • ③3环调用CloseHandle关闭设备
      • 0环会执行IRP_MJ_CLOSE对应的派遣函数
//pLinkNane是0环创建的设备符号链接( "\\\\.\\MyTestDriver" )
hHandle = CreateFile(pLinkNane,GENERIC_READ|GENERIC _WRITE,0,0,OPEN_EXISTING ,FILE_ ATTRIBUTE_ NorMAL,0);
  • 代码:
    • 0环代码
      #include <ntddk.h>
      
      // 定义设备名和符号名
      #define DEVICE_NAME L"\\Device\\MTReadDevice_asajs123akdas"
      #define SYM_LINK_NAME L"\\??\\MTRead_asdkasjkadsjldasss213k"
      
      // 设备创建函数
      NTSTATUS DeviceCreate(PDEVICE_OBJECT Device, PIRP pIrp){
          __asm int 3
          pIrp->IoStatus.Status = STATUS_SUCCESS;
          pIrp->IoStatus.Information = 0;
          // I/O请求处理完毕
          IoCompleteRequest(pIrp, IO_NO_INCREMENT);
          DbgPrint("Create Device Success\n");
          return STATUS_SUCCESS;
      }
      
      // 设备读操作函数
      NTSTATUS DeviceRead(PDEVICE_OBJECT Device, PIRP pIrp){
          __asm int 3
          // 获取指向IRP的堆栈的指针
          PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
          // 获取堆栈长度
          ULONG length = stack->Parameters.Read.Length;
          pIrp->IoStatus.Status = STATUS_SUCCESS;
          pIrp->IoStatus.Information = length;
          // 将堆栈上的数据全设置为0xAA
          memset(pIrp->AssociatedIrp.SystemBuffer, 0xBB, length);
          IoCompleteRequest(pIrp, IO_NO_INCREMENT);
          DbgPrint("Read Device Success\n");
          return STATUS_SUCCESS;
      }
      
      // 设备关闭函数
      NTSTATUS DeviceClose(PDEVICE_OBJECT Device, PIRP pIrp){
          __asm int 3
          // 跟设备创建函数相同
          pIrp->IoStatus.Status = STATUS_SUCCESS;
          pIrp->IoStatus.Information = 0;
          IoCompleteRequest(pIrp, IO_NO_INCREMENT);
          DbgPrint("Close Device Success\n");
          return STATUS_SUCCESS;
      }
      
      // 驱动卸载函数
      NTSTATUS DriverUnload(PDRIVER_OBJECT Driver){
          NTSTATUS status;
          __asm int 3
          // 删除符号和设备
          UNICODE_STRING SymLinkName;
          RtlInitUnicodeString(&SymLinkName, SYM_LINK_NAME);
      
          status = (IoDeleteSymbolicLink(&SymLinkName));
          DbgPrint("删除状态码:%x", status);
      
          PDRIVER_OBJECT pFirstObj = Driver->DeviceObject;
          if (pFirstObj->DeviceType == FILE_DEVICE_COMPORT)
          {
          }
          IoDeleteDevice(Driver->DeviceObject);
          DbgPrint("This Driver Is Unloading...\n");
          return STATUS_SUCCESS;
      }
      
      // 驱动入口函数
      NTSTATUS DriverEntry(PDRIVER_OBJECT Driver, PUNICODE_STRING RegPath){
          __asm int 3
          NTSTATUS status;
      
          UNICODE_STRING DeviceName;
          UNICODE_STRING SymLinkName;
          // 将设备名转换为Unicode字符串
          RtlInitUnicodeString(&DeviceName, DEVICE_NAME);
          // 创建设备对象
          PDEVICE_OBJECT pDevice = NULL;
          Driver->DriverUnload = DriverUnload;
          status = IoCreateDevice(Driver, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, 
                                  , &pDevice);
          if (!NT_SUCCESS(status))
          {
              IoDeleteDevice(pDevice);
              DbgPrint("Create Device Faild!\n");
              return STATUS_UNSUCCESSFUL;
          }
          // 设置pDevice以缓冲区方式读取
          pDevice->Flags = DO_BUFFERED_IO;
      
          // 将符号名转换为Unicode字符串
          RtlInitUnicodeString(&SymLinkName, SYM_LINK_NAME);
          // 将符号与设备关联
          status = IoCreateSymbolicLink(&SymLinkName, &DeviceName);
          if (!NT_SUCCESS(status))
          {
              DbgPrint("Create SymLink Faild!\n");
              IoDeleteDevice(pDevice);
              return STATUS_UNSUCCESSFUL;
          }
      
          DbgPrint("Initialize Success\n");
      
          // 注册设备创建函数、设备读函数、设备关闭函数、驱动卸载函数
          Driver->MajorFunction[IRP_MJ_CREATE] = DeviceCreate;
          Driver->MajorFunction[IRP_MJ_READ] = DeviceRead;
          Driver->MajorFunction[IRP_MJ_CLOSE] = DeviceClose;
      
          DbgPrint("Initialize Success\n");
      
          return STATUS_SUCCESS;
      }
    • 3环代码
      #include<windows.h>
      #include<winioctl.h>
      #include<stdio.h>
      
      #define IN_BUFFER_MAXLENGTH  0x10
      #define OUT_BUFFER_MAXLENGTH  0x10
      //宏定义:获取一个32位的宏控制码(dwIoCode),操作码需要与0环定义的一致,0环中的派遣函数会获取3环发送的操作码,然后判断是否一致,再进行相应的操作
      //参数1:设备类型(鼠标,键盘,无类型等等);
      //参数2:0x000-0x7FF保留,0x800-0xfff随便填一个;
      //参数3:数据交互类型(缓冲区,IO,其他);
      //参数4:对这个设备的权限
      #define OPER1 CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
      #define OPER2 CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)
      //设备符号链接
      #define SYMBOLICLINK_NAME  "\\\\.\\MyTestDriver"  
      //全局驱动句柄
      HANDLE g_hDevice;
      
      //打开驱动服务句柄
      //3环链接名:\\\\.\\AABB
      BOOL Open(PCHAR pLinkName)
      {
      	//在3环获取设备句柄
      	TCHAR szBuffer[10] = { 0 };
      	g_hDevice = ::CreateFile(pLinkName, GENERIC_READ | GENERIC_WRITE, 0,0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
      	DWORD err = ::GetLastError();
      	sprintf(szBuffer, "%d\n", err);
      	if (g_hDevice != INVALID_HANDLE_VALUE)
      		return TRUE;
      	else
      		return FALSE;
      
      }
      BOOL IoControl(DWORD dwIoCode, PVOID InBuff, DWORD InBuffLen, PVOID OutBuff, DWORD OutBuffLen)
      {
      	//实际返回长度
      	DWORD dw = 0;
      	//设备句柄/控制码/输入缓冲区地址/输入缓冲区长度/输出缓冲区地址/输出缓冲区长度/返回长度/指向OVERLAPPED 此处为空
      	//这一串东西最后会转成IRP发生给设备,设备再从IRP中分解出数据
      	DeviceIoControl(g_hDevice, dwIoCode, InBuff, InBuffLen, OutBuff, OutBuffLen, &dw,NULL);
      	return TRUE;
      }
      int main(int argc, char* argv[]){
      	DWORD dwInBuffer = 0x11112222;
      	TCHAR szOutBuffer[OUT_BUFFER_MAXLENGTH] = { 0 };
      	//1.通过符号链接,打开设备
      	Open((PCHAR)SYMBOLICLINK_NAME);
      	//2.开始通信
      	IoControl(OPER2, &dwInBuffer, IN_BUFFER_MAXLENGTH, szOutBuffer, OUT_BUFFER_MAXLENGTH);
      	printf("%s", szOutBuffer);
      	//3.关闭设备
      	CloseHandle(g_hDevice);
      }
      

4. HOOK

4.1 SSDT HOOK

4.1.1 SystemServiceTable

4.1.2 访问系统服务表

  • 测试导出全局变量KeServiceDescriptorTable,获取SSDT表
获取的KeServiceDescriptorTable值是80553180
查看函数地址

4.1.3 SSDT HOOK NtOpenProcess

  • 1.通过IDA打开kernel32找到NtOpenProcess函数
  • 2.跟踪进去,找到系统服务号
  • 3.自己定义SST和SSDT结构体,获取SSDT,找到函数地址表
  • 4.根据系统服务号,找到函数地址表对应的下标,这个就是NtOpenProcess函数地址
    • NtOpenProcess的系统服务号是7A
    • kd> dd 80502030+7A*4得到NtOpenProcess函数地址
    • 代码:函数地址表[7A]
  • 5.物理页的属性有些是受保护的,不可写
    • 方法1:修改PDT属性为可写(可以无视多核)
    • 方法2:修改R0寄存器的PW保护位(多核时需要谨慎,每一个核有一套控制寄存器,切换核的时候会有风险)
  • 6.将这个地址改成我们自己定义的函数地址(与NtOpenProcess原型一样的函数),并且备份原来的函数地址
    //定义函数指针,在自己的函数中调用原函数
    typedef NTSTATUS(*NTOPENPROCESS) (
    	PHANDLE
    	ProcessHandle,
    	ACCESS_MASK DesiredAccess,
    	POBJECT_ATTRIBUTES Objectttributes,
    	PCLIENT_ID ClientId);
  • 7.恢复程序,取消HOOK(驱动卸载的时候调用)

4.1.4 SSDT HOOK的缺点

  • 1.容易发现,容易绕过
  • 2.只能HOOK系统服务表里面有的函数

4.2 Inline HOOK

  • 与3环的Inline HOOK一致

5. 多核同步

5.1 临界区

  • Inline HOOK的时候,如果刚好发生多核切换,那么就会蓝屏

5.1.1 并发与同步

  • 并发是指多个线程在同时执行:
    • 单核(是分时执行,不是真正的同时)
    • 多核(在某一个时刻,会同时有多个线程再执行)
  • 同步
    • 同步则是保证在并发执行的环境中各个线程可以有序的执行

5.1.2 LOCK指令

DWORD dwVal= 0; 	//全局变量
线程中的代码:
	dwVal ++;	//只有一行安全吗?
对应的汇编代码:
    mov eax,[0x12345678]
    add eax,1
    mov [0x12345678],eax
有三条指令,不能保证执行过程中没有线程切换,不安全
  • LOCK指令
    INC DWORD PTR DS:[0x12345678] //一行汇编代码,安全吗?
    • 单核情况下,是安全的,多核情况下可能存在两个CPU同时执行一行代码,假设0x12345678此时数值为9,那么两个cpu同时对9+1,执行完之后,本应该是11,但实际上却是10
  • 改成
    LOCK INC DWORD PTR DS:[0x12345678]
    • LOCK是将内存锁住,LOCK时只能有一个核对该内存进行读写,LOCK只能锁住一行代码,多行代码不行
    • 参考: kernel32.InterlockedIncrement
  • 原子操作相关的APl:
    • InterlockedIncrement
    • InterlockedExchangeAdd
    • InterlockedDecrement
    • InterlockedFlushSL .ist
    • InterlockedExchange
    • InterlockedPopEntrySList
    • InterlockedCompareExchange
    • InterlockedPushEntrySList

5.1.3 自己实现临界区

  • 实现多核的线程同步

5.2 自旋锁

5.2.1 分析windows多核版本如何实现临界区

  • 首先判断[ecx]是否为0,为0说明当前没有线程进去临界区,可以进入,进入临界区时,将[ecx]设置为1
  • pause是降温,降低运行速度。使用pause是针对多核的,若是单核这样写的话,将一直死循环,多核就可以循环一段时间后,等待资源释放,这就是自旋锁
  • 小结
    • 自旋锁只能针对多核使用,对单核没有意义
    • 自旋锁与临界区、事件、互斥体一样,都是一种同步机制,都可以让当前线程处于等待状态,区别在于自旋锁不需要切换线程,只需要等待其他核使用完资源就可以进入临界区,更加轻量级,效率更高

6. 内核重载

6.1 内核重载解决的问题

  • 内核重载可以绕过HOOK

6.2 钩子如何绕过

  • 1.哪里被挂钩子了,我们在被挂钩子的地方再次挂多一个钩子,绕过它
  • 2.内核重载
  • 3.自己实现该函数的功能

6.3 内核重载

  • 如果想重载ntoskrnl.exe就加载ntoskrnl.exe,重载Win32k.sys就加载Win32k.sys(根据被HOOK情况选择)

6.4 重载内核缺点

  • <1>改动太大,即使抹去PE指纹也无法完全隐形。太容易被找到,如:搜索系统函数的硬编码,都是搜索出2份的话,说明有内核重载
  • <2>最好的办法不是重载内核,而是需要什么函数自己来实现。


posted @ 2021-01-01 10:36  三一米田  阅读(515)  评论(0)    收藏  举报