怎么样看电脑里有没有外挂(驱动外挂的原理及检测手段)
因为PatchGuard技术的存在导致游戏在驱动层的保护不能像以前那样通过SSDTHook或者IDTHook来做了,游戏厂家不能擅自关闭PatchGuard来强行Hook.这样留给驱动挂的空间就很大了我将以一个自瞄挂的原理为例子展示驱动外挂的几种实现方式及检测手段
相同点
要想实现自瞄驱动挂基本上都是读取游戏数据然后直接操作鼠标,不同之处就是操纵鼠标的方式下面是所有驱动挂的几个相同之处
相同点1-获取鼠标的驱动对象
一个自瞄驱动挂的实现首先必然需要获得鼠标的驱动对象,在任何驱动挂里应该都是一样的可以通过ObReferenceObjectByName来获取鼠标驱动对象其中ObReferenceObjectByName是未公开的函数,声明一下就能用.其声明如下
NTSTATUSObReferenceObjectByName(PUNICODE_STRINGObjectName,ULONGAttributes,PACCESS_STATEAccessState,ACCESS_MASKDesiredAccess,POBJECT_TYPEObjectType,KPROCESSOR_MODEAccessMode,PVOIDParseContest,PVOID*Object);externPOBJECT_TYPE*IoDriverObjectType;//这是ObjectType参数的实参
其中鼠标驱动的名称为\\Driver\\MouClass那么具体获取鼠标驱动对象过程如下:
NTSTATUSstatus=STATUS_SUCCESS;UNICODE_STRINGmouseName;RtlInitUnicodeString(&mouseName,L"\\Driver\\MouClass");//获取到鼠标驱动对象将保存至此PDRIVER_OBJECTmouseDriver;status=ObReferenceObjectByName(&mouseName,OBJ_CASE_INSENSITIVE,NULL,0,*IoDriverObjectType,KernelMode,NULL,&mouseDriver);if(!NT_SUCCESS(status)){returnstatus;}else{//获取失败了需要解引用ObDereferenceObject(mouseDriver);}
这样一个鼠标驱动对象就在mouseName变量中了
相同点2-控制驱动挂的手段
用户要想控制驱动首先要通过CreateFile来打开驱动设备也就是说在驱动里必然先要创建好一个设备供用户打开,具体步骤如下
//设备名和符号名的定义#defineITRUTH_DEVICE_NAMEL"\\Device\\iTruth_Device_20d04fe0"#defineITRUTH_SYMB_NAMEL"\\DosDevice\\iTruth_Device"//函数里设备创建过程UNICODE_STRINGdev_name;RtlInitUnicodeString(&dev_name,ITRUTH_DEVICE_NAME);UNICODE_STRINGsddl;//SDDL语法请参考https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/sddl-for-device-objectsRtlInitUnicodeString(&sddl,L"D:P(A;;GA;;;BU)");//这个请自己定,每个驱动都不能一样GUIDdev_guid={0x3cff2c3aL,0x320f,0xf5aa,"iTruth"};//创建设备status=IoCreateDeviceSecure(DriverObject,0,&dev_name,FILE_DEVICE_UNKNOWN,FILE_DEVICE_SECURE_OPEN,FALSE,&sddl,&dev_guid,&iTruth_Device);if(NT_SUCCESS(status)){UNICODE_STRINGdos_dev_name;RtlInitUnicodeString(&dos_dev_name,ITRUTH_SYMB_NAME);//为设备绑定符号链接,用户只能通过这个符号链接打开设备IoCreateSymbolicLink(&dos_dev_name,&dev_name);//绑定处理函数DriverObject->MajorFunction[IRP_MJ_CREATE]=iTruth_DriverDispatch;DriverObject->MajorFunction[IRP_MJ_CLOSE]=iTruth_DriverDispatch;DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=iTruth_DriverDispatch;}
在用户层控制驱动基本上就是DeviceIoControl函数了,此函数向指定驱动发送IO控制码(CTL_CODE),其中控制码可以由一个名为CTL_CODE的宏来定义,这个宏的声明如下
#defineCTL_CODE(DeviceType,Function,Method,Access)(((DeviceType)<< 16) | ((Access) << 14) | ((Function) << 2) | (Method))
第一个参数是驱动的类型,驱动挂一般就填FILE_DEVICE_UNKNOWN即可.第二个是操作码,这个可以自已定.第三个是传递缓冲区的方式,METHOD_BUFFERED是比较简单的方式第四个是完成此动作需要的权限,不知道就填FILE_ANY_ACCESS下面是IOCTL的两个例子:
#defineIOCTL_ID_DBGPRINTCTL_CODE(FILE_DEVICE_UNKNOWN,0x700,METHOD_BUFFERED,FILE_WRITE_DATA)#defineIOCTL_ID_FUNCHOOKCTL_CODE(FILE_DEVICE_UNKNOWN,0x701,METHOD_BUFFERED,FILE_WRITE_DATA)
用户层的CTL_CODE和内核里的应该是一样的,所以这个控制码的定义一般单独放在一个头文件内驱动的入口函数是DriverEntry,其中第一个参数是PDRIVER_OBJECT类型的代表了当前驱动这个参数里有一个名为MajorFunction的数组,里面包含了各种驱动处理函数这个数组第__IRP_MJ_DEVICE_CONTROL__(0x0e)个元素是处理IO的回调函数,这个值是自己指定的一个典型的处理IRP_MJ_DEVICE_CONTROL的函数如下
NTSTATUSDriverDispatch(PDEVICE_OBJECTDeviceObject,PIRPIrp){NTSTATUSstatus=STATUS_SUCCESS;PIO_STACK_LOCATIONirp_loc=IoGetCurrentIrpStackLocation(Irp);if(DeviceObject==myDevice){if(irp_loc->MajorFunction==IRP_MJ_CREATE||irp_loc->MajorFunction==IRP_MJ_CLOSE){IoCompleteRequest(Irp,IO_NO_INCREMENT);returnSTATUS_SUCCESS;}for(PLIST_ENTRYpl=dev_list_head.Flink;pl!=(PLIST_ENTRY)&dev_list_head.Flink;pl=pl->Flink){PITRUTH_DEV_ENTRYdev_entry=CONTAINING_RECORD(pl,ITRUTH_DEV_ENTRY,dev_list);if(dev_entry->flt_dev_obj==DeviceObject){if(irp_loc->MajorFunction==IRP_MJ_DEVICE_CONTROL){switch(irp_loc->Parameters.DeviceIoControl.IoControlCode){//这里可以看到我们判断当前需要的功能部分caseIOCTL_ID_DBGPRINT:KdPrint((Irp->AssociatedIrp.SystemBuffer));Irp->IoStatus.Status=STATUS_SUCCESS;break;caseIOCTL_ID_FUNCHOOK:KdPrint(("Kernel:Hook\n"));Irp->IoStatus.Status=STATUS_SUCCESS;break;default:IoSkipCurrentIrpStackLocation(Irp);returnIoCallDriver(dev_entry->next_dev_obj,Irp);}}IoSkipCurrentIrpStackLocation(Irp);returnIoCallDriver(dev_entry->next_dev_obj,Irp);}}}
可以看到里面有根据我们的CTL_CODE来执行功能的switch语句,这个就是具体的控制原理
不同的鼠标控制手段
最简单的手段-通过过滤设备来控制鼠标
原理这种手段本质上是使用名为IoAttachDeviceToDeviceStack的函数将自己创建的设备对象绑定到设备对象链中的最高层,然后使用自己DriverDispatch回调函数来修改鼠标输入绑定过程如下:
//在相同点那里已经展示了鼠标驱动设备的获取方式,所以这里省略//获取鼠标驱动设备链中的第一个设备targetDevice=mouseDriver->DeviceObject;//遍历设备链中的所有设备while(targetDevice){//创建一个过滤设备status=IoCreateDevice(pDriver,sizeof(DEV_EXTENSION),NULL,targetDevice->DeviceType,targetDevice->Characteristics,FALSE,&filterDevice);if(!NT_SUCCESS(status)){filterDevice=targetDevice=NULL;returnstatus;}//在这步绑定nextDevice=IoAttachDeviceToDeviceStack(filterDevice,targetDevice);if(!nextDevice){IoDeleteDevice(filterDevice);filterDevice=NULL;returnstatus;}targetDevice=targetDevice->NextDevice;}
到了这步绑定好读取的分发函数那么鼠标的读取IRP就会发送到我们的驱动然后我们即可对其处理
pDriver->MajorFunction[IRP_MJ_READ]=MouseIRPMJRead;
下面是鼠标读取IRP的处理
NTSTATUSMouseIRPMJRead(PDEVICE_OBJECTpDevice,PIRPpIrp,PVOIDContext){UNREFERENCED_PARAMETER(pDevice);UNREFERENCED_PARAMETER(Context);PIO_STACK_LOCATIONstack;PMOUSE_INPUT_DATAmyData;stack=IoGetCurrentIrpStackLocation(pIrp);if(NT_SUCCESS(pIrp->IoStatus.Status)){//获鼠标盘数据myData=pIrp->AssociatedIrp.SystemBuffer;//这里即可开始读取游戏数据并更改鼠标的IRP}if(pIrp->PendingReturned){IoMarkIrpPending(pIrp);}returnpIrp->IoStatus.Status;}检测手段
毕竟是在设备栈上添加自己的设备,那么只需要一个设备黑名单即可.通过遍历设备栈只要找到了外挂创建的设备即判定非法判断的核心代码:
//设备对象的拓展结构typedefstruct_DEVICE_EXTENSION{PDEVICE_OBJECTpDevice;UNICODE_STRINGustrDeviceName;UNICODE_STRINGustrSymLinkName;}DEVICE_EXTENSION,*PDEVICE_EXTENSION;//检测代码targetDevice=mouseDriver->DeviceObject;//遍历设备链中的所有设备while(targetDevice){PDEVICE_EXTENSIONexdev=(PDEVICE_EXTENSION)targetDevice;//现在targetDevice->ustrDeviceName就是设备名了,下面即可自行判断这个设备名是否合法targetDevice=targetDevice->NextDevice;}
比较好的方式-Hook或直接调用MouseServiceClassCallBack
关键在于找到这个函数,寻找这个函数可以遍历设备对象也可以搜特征码遍历设备对象的寻找目标函数的方式如下
//全局定义MouseClassServiceCallbacktypedefVOID(*MouseClassServiceCallback)(PDEVICE_OBJECTDeviceObject,PMOUSE_INPUT_DATAInputDataStart,PMOUSE_INPUT_DATAInputDataEnd,PULONGInputDataConsumed);//保存原始函数MouseClassServiceCallbackorig_MouseClassServiceCallback=NULL;//我们已经获取过鼠标的设备驱动保存在了mouseDriver中,现在获取鼠标的端口驱动UNICODE_STRINGmouNtName;PDRIVER_OBJECTmouhidDriverObj;RtlInitUnicodeString(&mouNtName,L"\\Driver\\Mouhid");status=ObReferenceObjectByName(&mouNtName,OBJ_CASE_INSENSITIVE,NULL,0,IoDriverObjectType,KernelMode,NULL,&mouhidDriverObj);if(!NT_SUCCESS(status)){returnstatus;}//遍历mouclass下所有设备PDRIVER_OBJECTtargetDriverObj=mouseDriver->DeviceObject;ULONGmouDriverStart=(ULONG)GetModlueBaseAdress("mouclass.sys",0);ULONGmouDriverSize=0x2000;MouseClassServiceCallback*MouSrvAddr=NULL;while(targetDriverObj){//遍历我们先找到的端口驱动的设备扩展下的每个指针for(PBYTEexdev=(PBYTE)mouhidDriverObj;i<4096; ++i; exdev+=sizeof(PBYTE)) { if (!MmIsAddressValid(exdev)) { break; } //如果在设备扩展中找到一个地址位于mouClass模块中,就认为这是我们要的回调函数地址 PVOID tmp = *(PVOID*)exdev; if ((tmp >mouDriverStart)&&(tmp< (PBYTE)mouDriverStart+mouDriverSize)) { orig_MouseClassServiceCallback = (MouseClassServiceCallback)tmp; MouSrvAddr = (PVOID*)exdev; goto Done; } } targetDriverObj = targetDriverObj->NextDevice;}Done://这里就获取到了哪个函数的地址并保存到了orig_MouseClassServiceCallback中
现在我介绍那两种方式以及检测手段
Hook方式原理
直接改函数地址即可
*MouSrvAddr=myHookFuncAddr;
这种方式比较方便的地方是不用自己构建MOUSE_INPUT_DATA
检测手段
这种方式直接检测函数地址即可
直接调用方式原理
我们刚刚获取了函数指针那么直接调用就能使鼠标移动,麻烦的是要自己构造MOUSE_INPUT_DATA
检测手段
这种方式目前没有很好的检测手段,如果各位大佬有办法请务必让本菜见识下
杂谈
当然也是有通过HookIDT的鼠标中断来实现的,这种方式麻烦的地方在于要为CPU里每个核心都做一遍Hook操作.而且也能简单的通过特征码的方式简单的检测出来最重要的一点其实还在于如果做IDTHook那么还不如直接修改空闲中断的DPL和中断程序地址来做中断提权,让我们的外挂程序有Ring0权限.我感觉这样才是更好的办法