KbdClass键盘过滤驱动以及书中几处错误

驱动开发之 键盘过滤驱动--传统型键盘过滤 近来在看 《寒江独钓 windows内核编程》,看到键盘过滤部分,记下笔记,仅供参考,有理解不对之处,还望大家指正。 现在来说一下传统型键盘过滤,就是把自己的设备对象绑定在KbdClass设备对象之上。那么发送到KbdClass的IRP都会先经过自己的设备对象,我们可以在读派遣函数中设置完成例程,当IRP完成后在完成历程中得到按键信息。 KbdClass被称为键盘类驱动,在windows中,类 阅读详情

《寒江独钓》这本书中的键盘过滤驱动例子中,照着书上的例子抄完,代码有些错误的地方!

书上的例子有3处错误,分别如下:

1)对象类型声明错误

extern POBJECT_TYPE IoDriverObjectType;

修改为以下正确方式

extern POBJECT_TYPE *IoDriverObjectType;

在使用的*IoDriverObjectType地方,也需要修改成正确的方式

status = ObReferenceObjectByName (
        &uniNtNameString,
        OBJ_CASE_INSENSITIVE,
        NULL,
        0,
        *IoDriverObjectType, // 这里由IoDriverObjectType改成*IoDriverObjectType
        KernelMode,
        NULL,
        &KbdDriverObject
        );

2)解除对象引用计数,传入了错误的参数,应该传入KbdDriverObject

ObDereferenceObject(DriverObject);

改为

ObDereferenceObject(KbdDriverObject);

3)绑定过滤设备对象后,过滤设备对象的StackSize,不需要加1

pFilterDeviceObject->StackSize  = pLowerDeviceObject->StackSize+1;

msdn对于设备对象的StackSize字段解释中,第二句话指明了当使用IoAttachDevice或IoAttachDeviceToDeviceStack绑定一个设备对象时,IO管理器会自动绑定的设备对象设置合适的StackSize值,只有当前面是使用IoGetDeviceObjectPointer获取的设备对象,才需要显示声明它的设备对象StackSize+1;

Specifies the minimum number of stack locations in IRPs to be sent to this driver. IoCreateDevice and IoCreateDeviceSecure set this member to 1 in newly created device objects;

lowest-level drivers can therefore ignore this member. The I/O manager automatically sets the StackSize member in a higher-level driver’s device object to the appropriate value if the driver calls IoAttachDevice or IoAttachDeviceToDeviceStack.

Only a higher-level driver that chains itself over another driver with IoGetDeviceObjectPointer must explicitly set the value of StackSize in its own device object(s) to 1 + the StackSize value of the next-lower driver’s device object.

在调试时发现,刚创建的pFilterDeviceObject过滤设备对象,它的StackSize是1
在这里插入图片描述
经过IoAttachDeviceToDeviceStack函数绑定设备对象后,pFilterDeviceObject.StackSize被IO管理器设置成了4,而pLowerDeviceObject->StackSize此时是3
在这里插入图片描述
所以根本不需要再执行这一行代码:pFilterDeviceObject->StackSize = pLowerDeviceObject->StackSize+1;

最终键盘过滤效果:输入test
在这里插入图片描述
最终完整代码:

#include <ntifs.h>
#include <ntstrsafe.h>
#include <ntddkbd.h>

#define DELAY_ONE_MICROSECOND (-10)
#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
#define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000);


// IoDriverObjectType实际是一个全局遍历,但是头文件中没有,只要声明之后就可以使用了
extern POBJECT_TYPE *IoDriverObjectType;


// KbdCLass 驱动的名字
#define KBD_DRIVER_NAME L"\\Driver\\Kbdclass"


// 全局变量键盘计数
ULONG gC2pKeyCount = 0;


unsigned char asciiTbl[] = {
    0x00, 0x1B, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x2D, 0x3D, 0x08, 0x09,    //normal
    0x71, 0x77, 0x65, 0x72, 0x74, 0x79, 0x75, 0x69, 0x6F, 0x70, 0x5B, 0x5D, 0x0D, 0x00, 0x61, 0x73,
    0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x3B, 0x27, 0x60, 0x00, 0x5C, 0x7A, 0x78, 0x63, 0x76,
    0x62, 0x6E, 0x6D, 0x2C, 0x2E, 0x2F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    0x32, 0x33, 0x30, 0x2E,
    0x00, 0x1B, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x2D, 0x3D, 0x08, 0x09,    //caps
    0x51, 0x57, 0x45, 0x52, 0x54, 0x59, 0x55, 0x49, 0x4F, 0x50, 0x5B, 0x5D, 0x0D, 0x00, 0x41, 0x53,
    0x44, 0x46, 0x47, 0x48, 0x4A, 0x4B, 0x4C, 0x3B, 0x27, 0x60, 0x00, 0x5C, 0x5A, 0x58, 0x43, 0x56,
    0x42, 0x4E, 0x4D, 0x2C, 0x2E, 0x2F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    0x32, 0x33, 0x30, 0x2E,
    0x00, 0x1B, 0x21, 0x40, 0x23, 0x24, 0x25, 0x5E, 0x26, 0x2A, 0x28, 0x29, 0x5F, 0x2B, 0x08, 0x09,    //shift
    0x51, 0x57, 0x45, 0x52, 0x54, 0x59, 0x55, 0x49, 0x4F, 0x50, 0x7B, 0x7D, 0x0D, 0x00, 0x41, 0x53,
    0x44, 0x46, 0x47, 0x48, 0x4A, 0x4B, 0x4C, 0x3A, 0x22, 0x7E, 0x00, 0x7C, 0x5A, 0x58, 0x43, 0x56,
    0x42, 0x4E, 0x4D, 0x3C, 0x3E, 0x3F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    0x32, 0x33, 0x30, 0x2E,
    0x00, 0x1B, 0x21, 0x40, 0x23, 0x24, 0x25, 0x5E, 0x26, 0x2A, 0x28, 0x29, 0x5F, 0x2B, 0x08, 0x09,    //caps + shift
    0x71, 0x77, 0x65, 0x72, 0x74, 0x79, 0x75, 0x69, 0x6F, 0x70, 0x7B, 0x7D, 0x0D, 0x00, 0x61, 0x73,
    0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x3A, 0x22, 0x7E, 0x00, 0x7C, 0x7A, 0x78, 0x63, 0x76,
    0x62, 0x6E, 0x6D, 0x3C, 0x3E, 0x3F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    0x32, 0x33, 0x30, 0x2E
};



// 这个函数是存在的,只是文档中没有公开, 声明一下就可以直接使用了
// 这个函数是事实存在的,只是文档中没有公开。声明一下
// 就可以直接使用了。
NTSTATUS
ObReferenceObjectByName(
    PUNICODE_STRING ObjectName,
    ULONG Attributes,
    PACCESS_STATE AccessState,
    ACCESS_MASK DesiredAccess,
    POBJECT_TYPE ObjectType,
    KPROCESSOR_MODE AccessMode,
    PVOID ParseContext,
    PVOID* Object
);
typedef struct _C2P_DEV_EXT
{
    // 当前结构大小
    ULONG NodeSize;
    // 过滤设备对象
    PDEVICE_OBJECT pFilterDeviceObject;
    // 同时调用时的保护锁
    KSPIN_LOCK IoRequestsSpinLock;
    // 进程间同步处理
    KEVENT IoInProgressEvent;
    // 绑定的设备对象
    PDEVICE_OBJECT TargetDeviceObject;
    // 绑定前底层设备对象
    PDEVICE_OBJECT LowerDeviceObject;
}C2P_DEV_EXT,*PC2P_DEV_EXT;


NTSTATUS c2pDevExtInit(IN PC2P_DEV_EXT devExt, IN PDEVICE_OBJECT pFilterDeviceObject, IN PDEVICE_OBJECT pTargetDeviceObject, IN PDEVICE_OBJECT pLowerDeviceObject)
{
    memset(devExt, 0, sizeof(C2P_DEV_EXT));
    devExt->NodeSize = sizeof(C2P_DEV_EXT);
    devExt->pFilterDeviceObject= pFilterDeviceObject;
    devExt->TargetDeviceObject = pTargetDeviceObject;
    devExt->LowerDeviceObject = pLowerDeviceObject;


    KeInitializeSpinLock(&(devExt->IoRequestsSpinLock));
    KeInitializeEvent(&(devExt->IoInProgressEvent), NotificationEvent, FALSE);


    return STATUS_SUCCESS;
}


// 处理普通请求
NTSTATUS c2pDispatchGeneral(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    // 其他的分发函数直接skip,然后用IoCallDriver把IRP发送到真是设备的设备对象
    KdPrint(("Other Dispatch!"));
    IoSkipCurrentIrpStackLocation(Irp);
    return IoCallDriver(((PC2P_DEV_EXT)DeviceObject->DeviceExtension)->LowerDeviceObject, Irp);
}


// 处理Power请求
NTSTATUS c2pPower(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PC2P_DEV_EXT devExt;
    devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
    PoStartNextPowerIrp(Irp);
    IoSkipCurrentIrpStackLocation(Irp);
    return PoCallDriver(devExt->LowerDeviceObject, Irp);
}


// 处理PnP请求
NTSTATUS c2PnP(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{


    // 唯一需要处理的是,当有一个设备被拔出时,解除绑定,并删除过滤设备
    PC2P_DEV_EXT devExt;
    PIO_STACK_LOCATION irpStack;
    NTSTATUS status = STATUS_SUCCESS;
    KIRQL oldIrql;
    KEVENT event;


    // 获取真实设备
    devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
    irpStack = IoGetCurrentIrpStackLocation(Irp);


    // 对于其他类型的IRP,全部下发即可
    IoSkipCurrentIrpStackLocation(Irp);
    status = IoCallDriver(devExt->LowerDeviceObject, Irp);


    if (irpStack->MinorFunction == IRP_MN_REMOVE_DEVICE)
    {
        KdPrint(("IRP_MN_REMOVE_DEVICE \n"));
        // 然后解除绑定,并删除我们自己生成的虚拟设备
        IoDeleteDevice(devExt->LowerDeviceObject);
        IoDeleteDevice(DeviceObject);
        status = STATUS_SUCCESS;
    }
    return status;
}


// 键盘状态
#define S_SHIFT 1
#define S_CAPS  2
#define S_NUM   4


// 这是一个标记用来保存当前键盘状态,其中3个位,分别表示Caps Lock、 Num Lock、和Shift键是否按下了
static ULONG g_kbStatus = S_NUM;


// 从扫描码到ASCII
VOID MyPrintKeyStroke(UCHAR sch)
{
    UCHAR ch = 0;
    ULONG off = 0;


    if ((sch & 0x80) == 0) // 如果是按下
    {
        // 如果按下了字母或者数字等可见字符
        if ((sch < 0x47) || (sch >= 0x47 && sch < 0x54) && (g_kbStatus & S_NUM))
        {
            // 最终得到哪个字符必须由Caps Lock 和 Num Lock 以及Shift 这几个键的状态来决定,所以写在一张表中
            ch = asciiTbl[off + sch];
        }


        switch (sch)
        {
            // Caps Lock 和 Num Lock键类似,都是按下2次 等于没按过意义的反复键,所以这里用异或来这是标志。
        case 0x3A:
            g_kbStatus ^= S_CAPS;
            break;


            // 注意Shift的特点:
            // 1) Shift键有两个,左右各一个,扫描码不同
            // 2) Shift键是按下起作用, 谈起则作用消失
        case 0x2A:
        case 0x36:
            g_kbStatus |= S_SHIFT;
            break;
        case 0x45:
            g_kbStatus ^= S_NUM;
        }
    }
    else // break
    {
        // 按键弹起时,如果是shfist
        if (sch == 0xAA || sch == 0xB6)
            g_kbStatus &= ~S_SHIFT;
    }


    if (ch >= 0x20 && ch < 0x7F)
    {
        KdPrint(("%C \n", ch));
    }


}


// 处理Read请求完成回调函数
NTSTATUS c2pReadComplete(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
{
    // 读请求完成之后,应该获得输出缓冲器,按键信息就再输出缓冲区中,全局变量gC2pKeyCount应该减1,
    PIO_STACK_LOCATION IrpSp;
    ULONG ulTotalKeys = 0;
    PKEYBOARD_INPUT_DATA pKeyData = NULL;
    SIZE_T i;


    IrpSp = IoGetCurrentIrpStackLocation(Irp);


    // 假设这个请求是成功的,如果失败了,进一步获取信息是没有意义的
    if (NT_SUCCESS(Irp->IoStatus.Status))
    {


        // 获得读请求完成后的输出缓冲区
        pKeyData = Irp->AssociatedIrp.SystemBuffer;


        // 缓冲区中存在的按键数量
        ulTotalKeys = Irp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);


        // 这里可以做进一步处理,笔者只是很简单的打印出所有的扫描码
        for (i = 0; i < ulTotalKeys; i++)
        {
            // 下面打印按键的信息
            KdPrint(("TotalKeys: %u ", ulTotalKeys));


            // MakeCode就是扫描码
            KdPrint(("ScanCode: %#X ", pKeyData->MakeCode));


            // Flags值有很多种,这里只考虑按下和弹起两种情况,KEY_MAKE 表示按下、KEY_BREAK表示弹起
            KdPrint(("%s\n", pKeyData->Flags ? "Up" : "Down"));


            // 将扫描码,传入打印函数
            MyPrintKeyStroke((UCHAR)pKeyData->MakeCode);


            // 拦截实验,修改按下的Caps Lock,为Control






        }
    }


    gC2pKeyCount--;


    if (Irp->PendingReturned)
    {
        IoMarkIrpPending(Irp);
    }
    return Irp->IoStatus.Status;
}


// 处理Read请求
NTSTATUS c2pDispatchRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    NTSTATUS status = STATUS_SUCCESS;
    PC2P_DEV_EXT devExt;
    PIO_STACK_LOCATION currentIrpStack;
    KEVENT waitEvent;
    KeInitializeEvent(&waitEvent, NotificationEvent, FALSE);
    if (Irp->CurrentLocation == 1)
    {
        ULONG ReturnedInformation = 0;
        KdPrint(("Dispatch encountered bogus current location \n"));
        status = STATUS_INVALID_DEVICE_REQUEST;
        Irp->IoStatus.Status = status;
        Irp->IoStatus.Information = ReturnedInformation;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
        return status;
    }


    // 全局变量键盘计数器加1
    gC2pKeyCount++;


    // 得到设备扩展,目的时为了获得下一个设备的指针
    devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;


    // 设置回调函数并把IRP传递下去。 之后的读处理也就结束了 剩下的任务就是要等待读请求完成
    currentIrpStack = IoGetCurrentIrpStackLocation(Irp);
    IoCopyCurrentIrpStackLocationToNext(Irp);
    IoSetCompletionRoutine(Irp, c2pReadComplete, DeviceObject, TRUE, TRUE, TRUE);
    return IoCallDriver(devExt->LowerDeviceObject, Irp);
}


// 卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
    KdPrint(("Driver unloaded!!!\n"));


    // 把当前线程设置为低实时模式,以便让它的运行尽量的少影响其他程序
    PRKTHREAD CurrentThread = KeGetCurrentThread();
    KeSetPriorityThread(CurrentThread, LOW_REALTIME_PRIORITY);


    // 遍历我们驱动对象所创建的所有filter设备对象
    PDEVICE_OBJECT pDeviceObj = pDriverObject->DeviceObject;
    while (pDeviceObj)
    {
       
        // 解除绑定并删除所有的设备, 例如:我们绑定的是键盘驱动的1号设备对象,现在就需要detach(键盘驱动的1号设备对象),然后把绑定对应的filter对象也删掉
        PC2P_DEV_EXT devExt = (PC2P_DEV_EXT)pDeviceObj->DeviceExtension;
        IoDetachDevice(devExt->TargetDeviceObject);
        IoDeleteDevice(pDeviceObj);
         // 拿到下一个设备对象
        pDeviceObj = pDeviceObj->NextDevice;

    }


    ASSERT(NULL == pDriverObject->DeviceObject);


    // delay some time
    LARGE_INTEGER lDelay = RtlConvertLongToLargeInteger(100 * DELAY_ONE_MILLISECOND);


    while (gC2pKeyCount)
    {
        KeDelayExecutionThread(KernelMode, FALSE, &lDelay);
    }


    KdPrint(("DriverEntry unload ok!\n"));
}


// 这个函数经过改造,能打开驱动对象kbdCLass,然后绑定它下面所有设备
NTSTATUS c2pAttachDevices(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    NTSTATUS status = 0;
    UNICODE_STRING uniNtNameString;
    PC2P_DEV_EXT devExt;
    PDEVICE_OBJECT pFilterDeviceObject = NULL;
    PDEVICE_OBJECT pTargetDeviceObject = NULL;
    PDEVICE_OBJECT pLowerDeviceObject = NULL;


    // 用来保存键盘驱动对象
    PDRIVER_OBJECT KbdDriverObject= NULL;


    // 初始化一个字符串,就是kdbClass驱动的名字
    RtlInitUnicodeString(&uniNtNameString, KBD_DRIVER_NAME);


    // 根据驱动名字,得到键盘驱动对象
    status = ObReferenceObjectByName(&uniNtNameString, OBJ_CASE_INSENSITIVE, NULL, 0, *IoDriverObjectType, KernelMode, NULL, &KbdDriverObject);
    // 如果失败了就直接返回
    if (!NT_SUCCESS(status))
    {
        KdPrint(("MyAttach: couldn't get the KbdClass Device Object\n"));
        return status;
    }
    else
    {
        // 调用ObReferenceObjectByName会导致对KbdDriverObject驱动对象的引用计数增加
        // 必须相应的调用解引用ObDereferenceObject
        ObDereferenceObject(KbdDriverObject);
    }


    // 这是键盘驱动对象的设备链中的第一个设备对象
    pTargetDeviceObject = KbdDriverObject->DeviceObject;


    // 现在开始遍历这个设备链
    while (pTargetDeviceObject)
    {
        // 在我们的驱动对象上,生成一个过滤设备对象pFilterDeviceObject
        status = IoCreateDevice(DriverObject, sizeof(C2P_DEV_EXT), NULL, pTargetDeviceObject->DeviceType, pTargetDeviceObject->Characteristics, FALSE, &pFilterDeviceObject);


        // 如果失败了就直接退出
        if (!NT_SUCCESS(status))
        {
            KdPrint(("MyAttach: Couldn't create pFilterDeviceObject\n"));
            return status;
        }


        KdPrint(("Filter DO,%#X\n",pFilterDeviceObject));


        // 绑定。将我们创建的过滤设备对象pFilterDeviceObject,附加到键盘驱动的设备对象链的最顶端,并把之前最顶端的设备对象返回
        pLowerDeviceObject = IoAttachDeviceToDeviceStack(pFilterDeviceObject, pTargetDeviceObject);


        // 如果绑定失败,删除之前创建的过滤设备对象,并退出
        if (!pLowerDeviceObject)
        {
            KdPrint(("MyAttach: Couldn't attach to pFilterDeviceObject Device Object\n"));
            IoDeleteDevice(pFilterDeviceObject);
            pFilterDeviceObject = NULL;
            return status;
        }


        // 设备扩展。 下面要详细讲述设备扩展的应用
        devExt = (PC2P_DEV_EXT)(pFilterDeviceObject->DeviceExtension);


        // 初始化过滤设备扩展信息
        c2pDevExtInit(devExt, pFilterDeviceObject, pTargetDeviceObject, pLowerDeviceObject);


        // 下面的操作和前面过滤串口的操作基本一致,这里不再解释了
        pFilterDeviceObject->DeviceType         = pLowerDeviceObject->DeviceType;
        pFilterDeviceObject->Characteristics = pLowerDeviceObject->Characteristics;
        pFilterDeviceObject->Flags            |= pLowerDeviceObject->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE);


        // 移动到下一个设备,继续遍历
        pTargetDeviceObject = pTargetDeviceObject->NextDevice;


    }


    return status;
}


NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT pDriverObject, _In_ PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(pDriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
    ULONG i;
    NTSTATUS status;
    KdPrint(("c2p.sys: entering DriverEntry\n"));
    KdPrint(("Driver loaded!!\n"));


    // 填写所有的分发函数的指针
    for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
    {
        pDriverObject->MajorFunction[i] = c2pDispatchGeneral;
    }


    // 单独填写一个Read分发函数。因为重要的过滤就是读取来的按键信息,其他的都不重要,这个分发函数单独写。
    pDriverObject->MajorFunction[IRP_MJ_READ] = c2pDispatchRead;


    // 单独地填写一个IRP_MJ_POWER函数,这是因为这类请求中间要调用一个PoCallDriver和一个PoStartNextPowerIrp,比较特殊
    pDriverObject->MajorFunction[IRP_MJ_POWER] = c2pPower;


    // 我们想知道声明时候我们绑定过的一个设备被卸载了(比如从机器上被拔掉了),所以专门写一个PNP(即插即用)分发函数
    pDriverObject->MajorFunction[IRP_MJ_PNP] = c2PnP;


    // 卸载函数
    pDriverObject->DriverUnload = DriverUnload;


    DbgBreakPoint();


    // 绑定所有键盘设备
    status = c2pAttachDevices(pDriverObject, RegistryPath);
    
    return status;
}
《Windows内核安全与驱动编程》-第八章-键盘的过滤学习-day3 键盘的过滤 8.5 Hook 分发函数 ​ 前面讲述了进行键盘的过滤。本节开始更加深入地探讨键盘的过滤与反过滤的对抗。无论是过滤还是反过滤,其原理都是进行过滤。取胜的关键在于: 谁将第一个得到消息。 ​ 前面学到的键盘过滤方法,是通过在设备栈上绑定一个新的设备实现的。这样就有了一个简单的设想来防止黑客对键盘进行过滤: 检查设备栈,看上面是否有不明的设备。 ​ 但是一般的黑客软件如果想进行过滤的话,... 阅读详情

相关推荐

Windows 驱动开发:借壳通信技术实践详解

本文详细介绍了 Windows 驱动开发中的借壳通信技术,通过示例代码展示了如何使用 ObReferenceObjectByName 函数实现借壳通信以及如何处理 IRP 控制请求。同时,我们回答了关于驱动对象类型、保存原有处理函数以及处理 IRP 控制请求的问题。希望本文能够帮助您更好地理解和应用借壳通信技术。

qq_40363731的博客 405

利用IoDriverObjectType控制内核驱动加载

在edr或者其他类型的安全软件我们通常要监测当前系统的内核驱动的加载通常使用的方法是PsSetLoadImageNotifyRoutine设置模块加载回调例程来监控ring3模块以及ring0模块的加载,回调函数 eLoadImageNotifyRoutine 的第二个参数判断,如果 PID是0 ,则表示加载驱动,如果PID非零,则表示加载DLL。缺点当然也就是函数太底层,第二就是方法太通用几乎做过进程、线程监控的搞安全内核开发的人基本都晓得,也很容易被发现而且也会被摘链,而失效。...

weixin_60043341的博客 609

Windows键盘失灵代码10:注册表UpperFilters/LowerFilters修复指南

在Windows系统中,注册表作为核心配置数据库,存储着硬件驱动加载的关键信息。驱动加载机制通过UpperFilters(上层过滤器)和LowerFilters(下层过滤器)值构建驱动栈层次结构,确保设备正常工作。当第三方软件(如安全工具或外设管理程序)安装或卸载不当,可能在UpperFilters中残留无效驱动名,导致系统加载驱动栈时失败,引发设备管理器报错“代码10”。这种注册表配置问题常见于键盘、USB设备等输入/输出硬件,表现为设备无法识别或功能异常。本文针对键盘驱动加载失败的具体案例,深入解析Up

weixin_34168880的博客 496

驱动开发 键盘过滤驱动程序-- 传统的键盘过滤

最近看 《树木和独钓 windows内核编程》,看到键盘过滤部分,做笔记,仅供参考,那里被理解为是不正确的,同时,我们也希望大家指正。 如今来说一下传统型键盘过滤,就是把自己的设备对象绑定在KbdClass设备对象之上。那么发送到KbdClass的IRP都会先经过自己的设备对象,我们能够在读派遣函数中设置完毕例程,当IRP完毕后在完毕历程中得到按键信息。 KbdClass被称为键盘类驱...

weixin_34391854的博客 411

驱动级鼠标键盘控制:从原理到实战,突破用户层自动化限制

在自动化测试、游戏辅助和工业控制等领域,程序化控制鼠标键盘是常见需求。用户层模拟方案(如pyautogui、SendInput)通过系统API发送输入事件,但易受系统焦点、权限和应用程序校验的影响,在复杂环境下稳定性不足。其根本原因在于它们工作在用户模式(User Mode),无法模拟硬件级别的信号。驱动级输入模拟通过在内核层(Kernel Mode)创建虚拟输入设备(如HID设备),直接向系统输入子系统注入硬件事件,从而绕过用户层限制,实现高兼容性和抗干扰能力。这项技术对于需要7x24小时稳定运行、对抗复

weixin_34413103的博客 446

Windows键盘输入异常排查与修复指南

键盘输入异常是Windows系统中常见的硬件交互问题,其核心原理涉及键盘驱动、输入法服务和系统注册表的协同工作机制。当字母键失效而数字键正常时,往往表明系统底层的输入处理流程出现异常。从技术实现来看,操作系统通过kbdclass.sys驱动管理硬件信号,由ctfmon.exe进程处理输入法转换,最终通过注册表键盘布局映射生成有效输入。这类故障在游戏模式激活、远程桌面连接或多显示器环境等特定场景下尤为高发。通过系统文件检查(sfc)、注册表修复等工程实践手段,配合Alt+Shift等快捷键组合,能有效解决80

405

[Win11]解决PS/2标准键盘带感叹号,笔记本键盘无法使用。代码19:由于其配置信息(注册表中的)不完整或已损坏,Windows 无法启动这个

解决PS/2标准键盘带感叹号,笔记本键盘无法使用。代码19:由于其配置信息(注册表中的)不完整或已损坏,Windows 无法启动这个。

小浝仔 2万+

Windows内核键盘过滤驱动:VC++可调试源码与IRP拦截实战

Windows内核驱动是操作系统最底层的扩展机制,其核心在于通过IRP(I/O Request Packet)管理设备通信,并在不同IRQL级别下安全操作硬件与用户缓冲区。掌握键盘过滤、ObRegisterCallbacks进程监控、DriverEntry资源初始化等关键技术,不仅能实现按键拦截与数据注入,更支撑安全软件、EDR终端防护、输入法底层改造等高价值场景。本文基于真实可运行的VC++内核驱动工程,详解WHQL兼容的x64驱动构建链路、MDL/ProbeForRead内存安全访问、DISPATCH_

weixin_31364787的博客 261

删除、卸载虚拟机之后键盘驱动无效,黄色感叹号

最近,有朋友的电脑脑突然出现只要一按键盘就无反应,可电脑其他的程序还在运行,只是键盘鼠标无任何反应。而且重启以后不动键盘,鼠标可以做任何操作。使用360系统修复后没有解决问题。查看设备管理器,发现键盘驱动显示黄色叹号!后来重装驱动,显示“驱动不存在,错误代码39”,卸载之后还是依然。后来终于在网上找到了解决方法,为大家附上。键盘驱动出现黄色感叹号解决方法:出现问题的原因可能是因为删除虚拟...

dbikzroaawpqkk7943的博客 780

Windows屏蔽Ctrl+Alt+Del、Alt+Tab、Ctrl+Esc:从钩子到驱动全方案

在Windows桌面开发与运维中,屏蔽Ctrl+Alt+Del、Alt+Tab、Ctrl+Esc等组合键是实现kiosk模式、考试系统、工控HMI等单任务终端的关键技术。其原理基于Windows键盘输入链路:Alt+Tab和Ctrl+Esc属于应用层消息,可通过低级键盘钩子(WH_KEYBOARD_LL)或窗口消息拦截;而Ctrl+Alt+Del是安全注意序列(SAS),由Winlogon在更早环节处理,必须借助组策略、辅助功能程序替换或键盘过滤驱动。合理运用这些技术,能有效防止用户误操作或跳出应用,保障终

weixin_34081595的博客 347

k8s重装1.28.2+calico+踩坑收集+InPlacePodVerticalScaling

它提供了从镜像构建(通过 Dockerfile)、镜像管理(上传、下载、存储等)、容器创建、运行、网络配置(可以为容器分配自定义的网络,如 bridge 网络、host 网络等)到容器编排(通过 Docker Compose 可以简单地编排多个容器的启动和关联关系)等一整套完整的解决方案。它的核心功能是启动和管理容器的生命周期。sudo apt update这个可以经常用,是来更新软件包的,它不下载,所以没风险,但是经常会报错,别问,问就是某种强大的力量,所以用镜像或者代理或者巴拉巴拉。

SsjjsS2019的博客 2553

键盘驱动出现黄色感叹号解决方法

最近发现公司一台机器,由于员工胡乱删除文件,导致键盘驱动显示黄色叹号,右键点击属性显示错误代码39,卸载后还是不行,于是就从网上找到了解决方法: 第一步:打开注册表:regedit 注:键盘不能用,怎么输入呢? 方法1:依次点击开始菜单->所有程序->附件->轻松访问->屏幕键盘 方法2:C:\Windows\System32\regedt32.exe (此方法是开始注册表) 第二步:找到如下选项 HKEY_LOCAL_MACHINE\SYSTEM\Current..

mamengna的博客 5610

Minitab田口设计实战:如何用正交试验优化遗传算法参数(附完整操作步骤)

本文详细介绍了如何利用Minitab田口设计进行遗传算法参数优化,通过正交试验方法显著提升效率。文章涵盖实验设计、Minitab操作步骤、结果分析及高级技巧,帮助工程师快速掌握这一工业级优化技术,实现算法性能的显著提升。

wind6的博客 865

PS/2键盘代码10无法启动?从驱动到硬件的全面排查指南

设备管理器中的黄色感叹号常常让人不知所措,而“PS/2标准键盘状态为该设备无法启动。(代码10)”更是困扰着许多台式机和笔记本用户。代码10是Windows设备管理机制中的通用错误码,本质上是PnP管理器在向设备驱动发送启动请求时未能得到有效响应。理解这一原理后,我们可以通过设备管理器禁用启用、手动指定驱动、检查注册表过滤驱动、调整电源管理策略等系统级手段进行修复,多数情况下无需重装系统或更换硬件。针对笔记本内置键盘,还需考虑EC固件与排线接触问题。本文提供一套由浅入深的排查流程,帮助你快速定位并解决PS/

weixin_33483304的博客 242

西门子WinCC VBS脚本实战:5分钟搞定变量记录导出到Excel(附完整代码)

本文提供了一套高效的西门子WinCC VBS脚本方案,帮助工程师在5分钟内将变量记录数据导出至Excel。方案详细解析了时区转换、数据库连接、SQL查询优化等核心难点,并附带了完整、可直接使用的VBS代码,解决了工业自动化项目中数据导出的实际痛点。

moss5的博客 609

EM4095低频RFID曼彻斯特解码实战:从原理图到MCU代码实现

本文详细介绍了EM4095低频RFID读卡器的设计与实现,从芯片工作原理、天线设计到曼彻斯特解码算法。重点讲解了基于STM32的MCU代码实现,包括定时器捕获、数字滤波和错误处理,为开发125kHz RFID系统提供实用指南。

oil88的博客 714

笔记本键盘没反应(错误代码19)

4、再次打开注册表,定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4D36E96B-E325-11CE-BFC1-08002BE10318}项,添加多字符串值UpperFilters项,内容是kbdclass。1、临时使用电脑自带的辅助功能,屏幕键盘(WIN+CTRL+O可以打开),或者打开系统设置-辅助功能-键盘-屏幕键盘;5、再到设备管理器,找到键盘驱动,右键卸载驱动,后重启电脑即可。设备管理器里面报错:代码19。

烟雨平生的博客 218

cherry使用MCP协议Streamable HTTP实践

摘要:Streamable HTTP协议是针对传统HTTP+SSE传输方式的改进方案,解决了连接恢复、服务器资源占用和单向通信等问题。该协议基于普通HTTP实现,支持无状态服务器、纯HTTP服务和多轮对话,同时兼容现有基础设施。通过演示展示了服务端和客户端的配置方法,体现了其灵活性和实用性。MCP(多轮对话协议)作为扩展LLM能力的关键技术,代表着未来交互式AI的发展趋势。

全网120W+关注AI拉呱,专注人工智能以及科技前沿! 563

PS/2 标准键盘带感叹号【错误信息代码:30】

今天突然发现电脑自带键盘没办法使用,按CapsLock大小写键盘没有反应。查看了键盘键盘的状态出现了感叹号。个人出现这个问题原因是:昨天对PS/2标准键盘点击了禁用设备和启用设备,之后就出现了PS/2出现感叹号的问题。

qq_56800679的博客 1977

《手把手教你玩转RTOS操作系统》专栏简介

《手把手教你玩转RTOS操作系统》专栏系统讲解实时操作系统开发,以FreeRTOS为主线,结合RT-Thread和Zephyr对比分析。内容涵盖RTOS基础、任务管理、通信机制、中断处理等核心知识,并配套STM32/ESP32平台实战项目。专栏特色在于零基础友好、实战驱动、多系统对比和行业趋势分析,适合嵌入式开发者进阶RTOS开发能力,解决实际项目中的选型与优化问题。通过完整学习路径,读者可掌握构建高可靠性嵌入式系统的关键技能。

MHD0815的博客 531

在windows中安装并启动ActiveMQ5.16.2

打开ActiveMQ官网 下载ActiveMQ 点击apache-activemq-5.16.2-bin.zip进行下载 下载发后解压到c盘根目录下 在powershell中输入 ./activemq start启动 ActiveMQ服务 htpp://127.0.0.1:8161访问ActiveMQ控制台 ...

934

安装VMware,主机键盘不能用解决方法

不是第一次出现这个问题了,记录一下 右键电脑-属性-硬件-设备管理器,键盘那一项显示黄色叹号,属性显示为 由于其配置信息(注册表中的)不完整或已损坏,Windows 无法启动这个硬件设备 VMware安装以后会在C盘program files文件夹下生成VMware文件夹,卸载以后这个文件夹并不会消失掉,删掉这个文件夹系统会提示有文件受保护或正在运行,无法删除,强制或者修复VMware以后,再次开机键盘不能用了,但是外接usb键盘也不能用 解决方案 win10右键状态栏可开启 触摸键盘按钮 ,可正常打.

zgbhn的博客 6189

锁的艺术:Java并发中的常用锁策略与实践

对java锁相关内容进行整理分析:乐观锁 VS 悲观锁+自旋锁 VS 适应性自旋锁+无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁+公平锁 VS 非公平锁+可重入锁 VS 非可重入锁+独享锁 VS 共享锁

曾经“等你生日那天”都遥远得像未来,如今却可欢愉的挥手说“下个十年见” 171万+

键盘鼠标模拟完全指南:从消息层到驱动层的原理与选型

输入模拟是操作系统层面的事件注入,核心涵盖虚拟键码、扫描码及消息队列机制。理解不同层级的注入原理,是构建UI自动化、录放工具及跨平台辅助软件的基础。从消息层的PostMessage到API层的SendInput,再到驱动层与硬件HID方案,各层级在真实度、权限要求和适用范围上差异显著。在办公自动化、软件测试、外设开发等场景中,正确选型能有效规避焦点抢占、DPI偏移、输入法乱码等工程陷阱。本文系统梳理四种模拟方式的底层链路与实现细节,并给出基于实践经验的决策参考,帮助开发者根据真实需求选择最合理的输入模拟方案

weixin_34146805的博客 391

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

WIN7下PS/2等键盘失灵无法使用的解决办法--实测有效

插了PS2键盘发现键盘不能用了.以为键盘坏了,重启看机,一看能进bios,各键正常.然后再重启,进系统,看设备管理器,发现键盘为黄色惊叹号.查属性,为无法初始化硬件驱动.错误号冒失是28,有个未识别硬件。 以下为网络上的一个解决方案: 首先判断,驱动损坏.重新安装了驱动,重启,无效.手动指定安装,还是无效. 无耐去找解决方法.还真是不好弄. 现在把方法分享给大家: 运行注册表regedit

2万+

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

键盘注册表被删除怎么办?

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34124939的博客 2068

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

上一篇: vs2019编译openssl静态库
下一篇: 神秘的蓝屏代码0xc000007e
FFE4
博客等级 码龄9年 234粉丝 166原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值