0%

在内核驱动中模拟键盘按键(2)

简介

在驱动中模拟键盘按键的方法,包含 写端口 调用KeyboardClassServiceCallback函数 虚拟键盘 等。
其中 写端口 实现起来最简单,但是只能针对PS/2键盘。而 KeyboardClassServiceCallback 函数未导出,
需要我们不同系统去搜索不同的特征码,兼容性不佳。最后一个 虚拟键盘 稳定性最好,但是复杂度特别高。

写端口

在DOS时代,当我们按下或者放开一个键时,就会产生一个键盘中断(在允许键盘中断的情况下),这样程序
会跳转到BIOS中的键盘中断处理程序去执行。

打开windows设备管理器,可以查看到键盘控制器由两个端口控制。其中 0x60 是数据端口,可以读出键盘
数据,而 0x64 是控制端口,用来发出控制信号。也就是说,从 0x60 号端口可以读此键盘的按键信息。当
从这个端口读取一个字节,该字节的低7位就是按键的扫描码,而高1位则表示是按下键还是释放键。当按下
时,最高位为0,称为通码,当释放时,最高位为1,称为断码。

所以我们可以向这个端口写入数据,来模拟按键。如下为示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define I8042_DATA_PORT ((PUCHAR)0x60)
#define I8042_CTRL_PORT ((PUCHAR)0x64)
#define OBUFFER_FULL 0x01

BOOLEAN WaitKeyboardWrite()
{
INT i = 0;
UCHAR c = 0;
LARGE_INTEGER Interval = { 0 };
Interval.QuadPart = -50 * 1000 * 10L; // 50毫秒
for (i = 0; i < 1000; ++i)
{
KeDelayExecutionThread(KernelMode, FALSE, &Interval);
c = READ_PORT_UCHAR(I8042_CTRL_PORT);
if ((c & OBUFFER_FULL) == OBUFFER_FULL) break;
}
return i ? TRUE : FALSE;
}

void SendKeyScanCode(IN UCHAR Code)
{
// 等待缓冲区为空
WaitKeyboardWrite();
// 写入按键输入指令
WRITE_PORT_UCHAR(I8042_CTRL_PORT, 0xD2);
// 等待缓冲区为空
WaitKeyboardWrite();
// 写入按键扫描码
WRITE_PORT_UCHAR(I8042_DATA_PORT, Code);
}

回调函数

在驱动 kbdclass.sys 中,有个函数 KeyboardClassServiceCallback ,作用是往 读取按键的IRP
添加按键信息,并完成这个 IRP,这个函数的声明如下

1
2
3
4
5
6
VOID KeyboardClassServiceCallback(
_In_ PDEVICE_OBJECT DeviceObject,
_In_ PKEYBOARD_INPUT_DATA InputDataStart,
_In_ PKEYBOARD_INPUT_DATA InputDataEnd,
_Inout_ PULONG InputDataConsumed
);

参数 DeviceObject 为任意键盘设备,一般情况下我们使用 L"\\Device\\KeyboardClass0" 这个设备,
InputDataStartInputDataEnd 表示输入的按键信息的 起始结束 地址,而 InputDataConsumed
表示按键信息的数量。按键信息结构体如下所示

1
2
3
4
5
6
7
typedef struct _KEYBOARD_INPUT_DATA {
USHORT UnitId;
USHORT MakeCode;
USHORT Flags;
USHORT Reserved;
ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;

我们只关注 MakeCodeFlags 两个参数,其中 MakeCode 是按键的扫描码,而 Flags 表示按键的动作,
KEY_MAKEKEY_E0 表示 普通键扩展键按下KEY_BREAKKEY_E1 表示相应的 弹起 动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Keyboard Scan Codes (Numerical Order)
---------------+---------------+---------------+---------------+---------------
HEX DEC keys |HEX DEC keys |HEX DEC keys |HEX DEC keys |HEX DEC keys
---------------+---------------+---------------+---------------+---------------
|10 16 Q |20 32 D |30 48 B |40 64 F6
01 1 ESC |11 17 W |21 33 F |31 49 N |41 65 F7
02 2 1 |12 18 E |22 34 G |32 50 M |42 66 F8
03 3 2 |13 19 R |23 35 H |33 51 , |43 67 F9
04 4 3 |14 20 T |24 36 J |34 52 . |44 68 F10
05 5 4 |15 21 Y |25 37 K |35 53 / |45 69 Num
06 6 5 |16 22 U |26 38 L |36 54 R Shift|46 70 Scroll
07 7 6 |17 23 I |27 39 ; |37 55 PrtSc |47 71 Home
08 8 7 |18 24 O |28 40 ' |38 56 Alt |48 72 Up
09 9 8 |19 25 P |29 41 ` |39 57 Space |49 73 PgUp
0A 10 9 |1A 26 [ |2A 42 L Shift|3A 58 Caps |4A 74 -
0B 11 0 |1B 27 ] |2B 43 \ |3B 59 F1 |4B 75 Left
0C 12 - |1C 28 |2C 44 Z |3C 60 F2 |4C 76 Center
0D 13 = |1D 29 CTRL |2D 45 X |3D 61 F3 |4D 77 Right
0E 14 bs |1E 30 A |2E 46 C |3E 62 F4 |4E 78 +
0F 15 Tab |1F 31 S |2F 47 V |3F 63 F5 |4F 79 End
---------------+---------------+---------------+---------------+---------------
50 80 Down | | | |
51 81 PgDn | | | |
52 82 Ins | | | |
53 83 Del | | | |
---------------+---------------+---------------+---------------+---------------

内存搜索

回调函数 KeyboardClassServiceCallback 并没有被导出,我们要想使用它,就只能通过特征码搜索,
首先需要找到 kbdclass.sys 驱动的基址,我们可以使用一个已导出但未文档的函数来获取

1
2
3
4
5
6
7
8
9
10
11
extern POBJECT_TYPE* IoDriverObjectType;

NTKERNELAPI NTSTATUS ObReferenceObjectByName(
IN PUNICODE_STRING ObjectName,
IN ULONG Attributes,
IN PACCESS_STATE AccessState,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN OUT PVOID ParseContext,
OUT PVOID* Object);

获取 键盘驱动键盘设备 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PDRIVER_OBJECT KeyboardDriver = NULL; // 键盘驱动对象
PDEVICE_OBJECT KeyboardDevice = NULL; // 键盘设备对象
PFILE_OBJECT KeyboardFile = NULL; // 键盘文件对象

NTSTATUS GetKeyboardDriverDevice()
{
NTSTATUS Status = STATUS_SUCCESS;
UNICODE_STRING KbdDriverName = RTL_CONSTANT_STRING(L"\\Driver\\kbdclass");
UNICODE_STRING KbdDeviceName = RTL_CONSTANT_STRING(L"\\Device\\KeyboardClass0");
// 获取键盘设备对象
Status = IoGetDeviceObjectPointer(
&KbdDeviceName, FILE_ALL_ACCESS, &KeyboardFile, &KeyboardDevice);
if (!NT_SUCCESS(Status)) return Status;
// 获取键盘驱动对象
Status = ObReferenceObjectByName(&KbdDriverName, OBJ_CASE_INSENSITIVE,
NULL, GENERIC_ALL, *IoDriverObjectType, KernelMode, NULL, &KeyboardDriver);
if (!NT_SUCCESS(Status))
{
// 注意IoGetDeviceObjectPointer只能解引用文件对象
ObDereferenceObject(&KeyboardFile);
return Status;
}
return STATUS_SUCCESS;
}

搜索回调函数的代码,其中特征码是通过 WinDbg 加载操作系统符号查找到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// KeyboardClassServiceCallback
typedef void(*KCSC)(PDEVICE_OBJECT, PKEYBOARD_INPUT_DATA, PKEYBOARD_INPUT_DATA, PULONG);
KCSC pKeyboardClassServiceCallback = NULL; // 回调函数指针

NTSTATUS SearchKeyboardClassServiceCallback()
{
ULONG Index = 0;
PUCHAR BaseAddr = KeyboardDriver->DriverStart;
ULONG DrvSize = KeyboardDriver->DriverSize;
for (Index = 0; Index < DrvSize; Index++)
{
if (*(PULONG)(BaseAddr + Index) != 0x8b55ff8b) continue;
if (*(PULONG)(BaseAddr + Index + 4) != 0x8b5151ec) continue;
if (*(PULONG)(BaseAddr + Index + 8) != 0x65830845) continue;
if (*(PULONG)(BaseAddr + Index + 12) != 0x8b530008) continue;
if (*(PULONG)(BaseAddr + Index + 16) != 0x5d2b105d) continue;
pKCSC = (KCSC)(BaseAddr + Index);
break;
}
if (pKCSC != NULL) return STATUS_SUCCESS;
return STATUS_UNSUCCESSFUL;
}

在WinXPSp3系统中,查找特征码步骤如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: kd> x kbdclass!KeyboardClassServiceCallback
f78b6192 kbdclass!KeyboardClassServiceCallback (<no parameter info>)

0: kd> dd f78b6192
f78b6192 8b55ff8b 8b5151ec 65830845 8b530008
f78b61a2 5d2b105d 708b560c 14458b28 57002083
f78b61b2 15ff046a f78b6ea8 ff6c4e8d 8b6ea415

0: kd> u kbdclass!KeyboardClassServiceCallback l 0xa
kbdclass!KeyboardClassServiceCallback:
f78b6192 8bff mov edi,edi
f78b6194 55 push ebp
f78b6195 8bec mov ebp,esp
f78b6197 51 push ecx
f78b6198 51 push ecx
f78b6199 8b4508 mov eax,dword ptr [ebp+8]
f78b619c 83650800 and dword ptr [ebp+8],0
f78b61a0 53 push ebx
f78b61a1 8b5d10 mov ebx,dword ptr [ebp+10h]
f78b61a4 2b5d0c sub ebx,dword ptr [ebp+0Ch]

最后 KeyboardClassServiceCallback 函数的用法如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void SendKeyInputExample()
{
ULONG InputDataConsumed = 0;
KEYBOARD_INPUT_DATA KbdInData[2] = { 0 };
// 发送按下A键
KbdInData[0].MakeCode = 0x1E; // A键
KbdInData[0].Flags = KEY_MAKE; // 按下
pKeyboardClassServiceCallback(
KeyboardDevice, &KbdInData[0], &KbdInData[1], &InputDataConsumed);
// 发送弹起A键
KbdInData[0].Flags = KEY_BREAK; // 弹起
pKeyboardClassServiceCallback(
KeyboardDevice, &KbdInData[0], &KbdInData[1], &InputDataConsumed);
}