Trao đổi với tôi

http://www.buidao.com

11/7/09

[Programming] Ứng dụng cơ chế Hook - Phần 1

Ứng dụng cơ chế Hook để xây dựng chương trình hỗ trợ gõ Tiếng Việt trên Hệ Điều Hành Windows 32 Bit - Phần 1

I.Sự kiện và thông điệp trên HĐH Windows

I.1.Giới thiệu

Ngày nay, Windows có lẽ không còn xa lạ với người sử dụng PC. Nhắc đến Windows, người ta thường nghĩ về nó như một Hệ Điều Hành (HĐH) dễ sử dụng, ở đó, tương tác giữa người dùng với các ứng dụng cũng như với các thành phần tiện ích của Windows thông qua giao diện đồ họa (GUI), bằng các thao tác trên keyboard, mouse vô cùng đơn giản.

Một câu hỏi được đặt ra là : “Các ứng dụng làm thế nào để phân loại, lưu giữ cũng như đáp lại những tương tác đó cho người dùng ?”. Đối với Windows vấn đề này được giải quyết một cách trọn vẹn : HĐH đưa ra cơ chế thông điệp (message) cùng với tập hợp các cấu trúc dữ liệu và các hàm API hỗ trợ ứng dụng trong việc giao tiếp với người dùng.

I.2.Hai loại hàng đợi thông điệp trên Windows

Trước hết, ta cần phải biết làm thế nào HĐH Windows lưu trữ các thông điệp. Hai loại hàng đợi trong Windows phục vụ cho mục đích này :

+ Hàng đợi hệ thống (The System Queue)

+ Hàng đợi thông điệp của ứng dụng (Application Queue)

Hàng đợi hệ thống (The System Queue)

Windows có các trình điều khiển thiết bị (Drivers) chịu trách nhiệm cho các dịch vụ ngắt từ thiết bị phần cứng mouse và keyboard. Tại mỗi thời điểm ngắt phát sinh, các Driver này gọi một điểm vào (hàm) đặc biệt trong USER.EXE để chỉ ra rằng một sự kiện vừa xảy ra.

Tất cả các sự kiện mouse, keyboard được lưu trong một hàng đợi được gọi là hàng đợi hệ thống (The System Queue). Đây là hàng đợi dùng chung cho toàn hệ thống , mọi tiến trình đang chạy đều chia xẻ hàng đợi này. Nhiệm vụ độc nhất của nó là ghi nhận lại những sự kiện phần cứng (mouse actions, keystrokes) khi chúng xảy ra.

Hàng đợi thông điệp của ứng dụng (Application Queue)

Khi một tiến trình được khởi tạo, một hàng đợi đại diện cho nó được tạo ra, hàng đợi này (đôi khi được gọi là hàng đợi tác vụ) được dùng để chứa những thông điệp sẽ được gởi cho các cửa sổ của ứng dụng. Những thông điệp này là những thông điệp được gởi một cách tường minh bằng một trong hai hàm sau :

+ PostMessage

+ PostAppMessage

(Lưu ý : Hàm PostQuitMessage không gởi thông điệp vào hàng đợi ứng dụng

Ứng dụng có thể dùng hai Primitive là GetMessagePeekMessage doWindows cung cấp để khảo sát hàng đợi của mình. Hai hàm này cho phép ứng dụng “Lấy một message ra khỏi hàng đợi” để từ đó phân loại và có những “Trả lời” thích hợp với người dùng.

I.3.Bàn thêm về GetMessage và PeekMessage

Bên trong HĐH Windows, GetMessagePeekMessage thi hành cùng mã lệnh. Điểm khác biệt chính giữa 2 hàm là trong trường hợp không có message nào được trả về cho ứng dụng. Trong trường hợp này, GetMessage đặt ứng dụng vào trạng thái “Sleep” trong khi PeekMessage trả về ứng dụng giá trị NULL.

Ngay trước khi GetMessagePeekMessage trả message về cho ứng dụng, hệ thống sẽ kiểm tra xem có sự tồn tại của hook WH_GETMESSAGE hay không. Nếu có một “Filter Function” (thực chất là 1 hàm) ứng với hook trên được cài đặt, hàm này sẽ được gọi. “Filter Function” không được gọi nếu PeekMessage không tìm thấy message và trả về NULL.

PeekMessage với tùy chọn PM_NOREMOVE

Theo mặc định, cả PeekMessage GetMessage sẽ lấy message ra khỏi hàng đợi khi mỗi message được trả về cho ứng dụng. Tuy nhiên, đôi lúc, ứng dụng có thể chỉ muốn vào hàng đợi để kiểm tra sự tồn tại của một messsage mà không muốn gỡ bỏ nó. Để làm được điều này, chương trình của ta sẽ gọi PeekMessage với tùy chọn PM_NOREMOVE, lúc này PeekMessage vẫn trả về message như thường lệ nhưng sẽ không lấy nó ra khỏi hàng đợi.(Chi tiết về GetMessage và PeekMessage có thể tham khảo tại Article “GetMessage and PeekMessage Internals” trong thư viện MSDN)

II.Sự kiện bàn phím

II.1.Mô hình chung

Mỗi phím trên bàn phím được hệ thống gắn với một giá trị duy nhất gọi là mã quét (Scan Code), đây là một giá trị phụ thuộc thiết bị, tức là giá trị này sẽ lệ thuộc vào từng loại bàn phím cụ thể. Khi người dùng gõ một phím, sẽ có 2 mã quét được sinh ra : một khi phím được nhấn và một khi phím được thả.

Trình điều khiển bàn phím (Keyboard Device Driver) sẽ thông dịch mã quét và chuyển nó thành mã phím ảo (vitual-key code), một giá trị độc lập thiết bị, được định nghĩa bởi hệ thống. Sau đó, trình điều khiển tạo một thông điệp bao gồm scancode, virtual-key code cùng một số thông tin khác (sự lặp phím, trạng thái các phím Alt, Ctrl...) , đặt vào hàng đợi hệ thống. Hệ thống lấy thông điệp này ra khỏi hàng đợi hệ thống và gởi đến hàng đợi thông điệp của ứng dụng. Cuối cùng, vòng lặp thông điệp (Message Loop) sẽ lấy thông điệp ra khỏi hàng đợi ứng dụng và chuyển nó đến hàm xử lý message thích hợp để xử lý. Quá trình trên có thể minh hoạ qua hình vẽ sau :

Rõ ràng là nếu ta có một cách nào đó để có được message trước khi nó đến hàm xử lý thông điệp của cửa sổ, ta có thể thực hiện được một vài thao tác thú vị. Và trên HĐH Windows, điều này hoàn toàn có thể thực hiện được nhờ cơ chế hook như ta sẽ thấy ở phần sau.

II.2.Các thông điệp liên quan bàn phím

Khi ta nhấn một phím, một thông điệp WM_KEYDOWN sẽ được đặt vào hàng đợi của tiểu trình ứng với cửa sổ đang được focus. Và khi ta nhả phím, hàng đợi sẽ có một thông điệp WM_KEYUP. Hai thông điệp này thường xuất hiện thành cặp. Tuy nhiên, nếu ta nhấn phím và giữ phím đủ lâu làm đặc tính “Automatic Repeat” của bàn phím hoạt động, hệ thống sẽ sinh nhiều thông điệp WM_KEYDOWN liên tiếp nhau. Và lưu ý một điều là khi chúng ta nhả phím, chỉ có một thông điệp WM_KEYUP được phát sinh mà thôi.

wParam của cả 2 thông điệp trên sẽ cho ta mã phím ảo (virtual-key code) của phím được nhấn.

lParam dùng 32 bit của mình để mô tả thông tin thêm về sự kiện gõ phím đã tạo ra message.

Thông tin này bao gồm : sự lặp phím, mã quét (scan code), trạn thái phím ALT, trạng thái phím trước,... Sơ đồ của 32 bit này như sau :

Repeat Count : Hệ thống tăng bộ đếm này lên khi bàn phím phát sinh thông điệp WM_KEYDOWN nhanh hơn khả năng xử lý message của ứng dụng. Điều này thường xảy ra khi người dùng giữ phím đủ lâu làm cho đặc tính “Automatic Repeat” của bàn phím hoạt động. Lúc này, thay vì đặt vào hàng đợi hệ thống nhiều thông điệp WM_KEYDOWN liên tiếp, hệ thống sẽ kết hợp các message này thành một message duy nhất và tăng giá trị Repeat Count. Tác vụ nhả phím không làm cho đặc tính “Automatic Repeat” được kích hoạt, do đó giá trị Repeat Count của thông điệp WM_KEYUP luôn luôn là 1.

Scan Code : Giá trị phụ thuộc thiết bị được bàn phím phát sinh khi một phím bị nhấn hay nhả. Đây không phải là mã ký tự đại diện cho phím được nhấn. Ứng dụng thường không quan tâm đến giá trị này, thay vì vậy, nó sử dụng giá trị độc lập thiết bị là mã phím ảo (virtual-key code) để xử lý message.

Extended-Key Flag Bit cờ hiệu cho biết một phím có phải là phím mở rộng hay không. Bit này được bật khi phím bị nhấn là phím mở rộng như : INS, DEL, HOME, END, ...

Context Code Bit cờ hiệu cho biết tại thời điểm thông điệp phát sinh, phím ALT có bị nhấn hay không. Bit này bật khi phím ALT bị nhấn, tắt khi phím ALT được nhả.

Previous Key-State Flag Bit cờ hiệu cho biết trạng thái của phím trước khi gởi message là “up” hay “down”. Bit này là 1 nếu phím là “down”, 0 nếu phím là “up”.

Transition-State Flag Bit này là 0 đối với WM_KEYDOWN, là 1 đối với WM_KEYUP.Thông điệp WM_KEYDOWN, WM_KEYUP cung cấp cho ta khá nhiều thông tin về sự kiện gõ phím, tuy nhiên, nó không cho ta mã ký tự của phím được nhấn. Để có được thông tin này, ứng dụng phải thêm hàm TranslateMessage vào vòng lặp thông điệp của mình.Hàm này chuyển thông điệp WM_KEYDOWN thành thông điệp WM_CHAR chứa mã ký tự ứng với phím được gõ, sau khi đã kiểm tra trạng thái của phím SHIFT và CAPS LOCK.

II.3.Giả lập gõ phím

Đôi khi chúng ta muốn có một sự kiện gõ phím mà không có sự tác động về mặt vật lý đối với bàn phím. Hàm keybd_event giúp ta có được khả năng này, chức năng của nó là giả lập một thao tác đối với bàn phím (nhấn hay nhả). Khai báo của hàm này như sau :

VOID keybd_event(

BYTE bVk, // virtual-key code

BYTE bScan, // hardware scan code

DWORD dwFlags, // function options

ULONG_PTR dwExtraInfo // additional keystroke data

);

Ý nghĩa của từng tham số :

bVk : mã phím ảo của phím cần giả lập.

bScan : tham số này không dùng.

dwFlags : xác định loại thao tác cần giả lập, tham số này là một trong các giá trị sau :

Giá trị

Ý nghĩa

KEYEVENTF_EXTENDEDKEY

Scan Code được đi trước bởi giá trị 224.

KEYEVENTF_KEYUP

Giả lập thao tác nhả phím.

dwExtraInfo : thông tin thêm về sự kiện ta giả lập.

III.Cơ chế Hook trên HĐH Windows

III.1.Giới thiệu

Trên HĐH Windows, một hook là cơ chế mà nhờ đó một hàm có thể chặn

các sự kiện (message, mouse actions, keystrokes) trước khi chúng đến

ứng dụng. Hàm này có thể thực hiện một số thao tác trên sự kiện, và

trong một vài trường hợp có thể định nghĩa lại hoặc hủy bỏ sự kiện mà

bắt được. Một đặc điểm quan trọng cần lưu ý là các hàm này được

gọi bởi bản thân HĐH Windows chứ không phải bởi ứng dụng.

Các hàm thuộc loại vừa nêu thường được gọi là Filter Function, chúng được phân loại theo loại sự kiện mà chúng can thiệp.Ví dụ, một Filter Function hứng tất cả các sự kiện mouse sẽ khác với một Filter Function can thiệp các sự kiện keyboard …

Một Filter Funtion muốn được Windows gọi thì trước tiên nó phải được gắn với một Windows Hook, công việc này thường được gọi là “Cài đặt một hook”. Windows Hook thật ra là các loại hook khác nhau được HĐH định nghĩa, nó giúp người sử dụng có thể cài đặt Filter Function của mình đúng với mục đích mà họ mong muốn. Khi cài đặt hook, ta xác định loại hook thông qua các hằng số có dạng WH_XXX, ví dụ WH_KEYBOARD xác định loại hook dùng để can thiệp các sự kiện thuộc về bàn phím.

Một Windows Hook có thể có nhiều Filter Function gắn với nó, trong tình huống này, Windows sẽ duy trì một chuỗi các Filter Function. Hàm được cài đặt gần nhất sẽ ở đầu chuỗi, hàm được cài đặt lâu nhất sẽ ở cuối chuỗi. Lúc này, nếu có một sự kiện xảy ra và sự kiện này tương ứng với loại hook ta đã cài đặt, Windows sẽ gọi hàm đầu tiên trong chuỗi các Filter Function ứng với hook đó.

III.2.Khả năng và ứng dụng của cơ chế Hook

Hook cung cấp các khả năng mạnh cho các ứng dụng chạy trên nền Windows, các ứng dụng này có thể dùng hook để :

· Xử lý hoặc định nghĩa tất cả các thông điệp cho dialog box, message box, scroll bar, hoặc menu của một ứng dụng. (Sử dụng hook WH_MSGFILTER).

· Xử lý hoặc định nghĩa tất cả các thông điệp cho dialog box, message box, scroll bar, hoặc menu của hệ thống. (Sử dụng hook WH_SYSMSGFILTER).

· Xử lý hoặc định nghĩa tất cả các thông điệp (bất chấp là thông điệp gì) của hệ thống mỗi khi GetMessage hoặc PeekMessage được gọi. (Sử dụng hook WH_GETMESSAGE)

· Xử lý hoặc định nghĩa tất cả các thông điệp (bất chấp là thông điệp gì) của hệ thống mỗi khi SendMessage được gọi. (Sử dụng hook WH_CALLWNDPROC).

· Thu (Record) và phát lại (Playback) các sự kiện keyboard và mouse . (Sử dụng hook WH_JOURNALRECORD, WH_JOURNALPLAYBACK).

· Xử lý , định nghĩa hoặc hủy bỏ tất cả các sự kiện bàn phím.(Sử dụng hook WH_KEYBOARD).

· Xử lý , định nghĩa hoặc hủy bỏ tất cả các sự kiện chuột (Sử dụng hook WH_MOUSE).

Tận dụng các khả năng trên, các ứng dụng có thể sử dụng hook để :

· Cung cấp phím trợ giúp F1 hỗ trợ menu, dialog box và message box (Sử dụng hook WH_MSGFILTER).

· Cung cấp các tính năng thu và phát các sự kiện mouse và keyboard, thường được gọi là các macro. (Sử dụng hook WH_JOURNALRECORD, WH_JOURNALPLAYBACK).

· Theo dõi các thông điệp để biết được thông điệp nào được gởi đến cửa sổ nào cũng như các hành động nào sẽ làm phát sinh thông điệp tương ứng. (Sử dụng hook WH_GETMESSAGE & WH_CALLWNDPROC). Chương trình Spy trong bộ Win32™ SDK của Windows NT đã rất thành công trong việc sử dụng hook để thực hiện tác vụ này.

· Giả lặp các tác vụ input của keyboard và mouse (Sử dụng hook WH_JOURNALPLAYBACK). Chỉ có hook mới có thể cho ta một phương pháp chắc chắn và tin cậy để thực hiện điều này. Nếu ta tiếp cận theo cách khác, gởi message chẳng hạn, Windows sẽ không cập nhật trạng thái của mouse cũng như keyboard, và điều này sẽ dẫn đến những hành động không mong muốn. Còn nếu như hook được sử dụng, các sự kiện sẽ được xử lý y hệt như sự kiện vật lý.

III.3.Quản lý chuỗi các Filter Function

Giao diện lập trình ứng dụng (API) của Windows cung cấp 3 hàm để thao tác với hook :

· SetWindowsHookEx

· UnhookWindowsHookEx

· CallNextHookEx

Cài đặt một Filter Function vào chuỗi các Filter Function của một hook

Tác vụ này được thực hiện thông qua hàm SetWindowsHookEx, khai báo của hàm này như sau :

HHOOK SetWindowsHookEx(

int idHook, // hook type

HOOKPROC lpfn, // hook procedure

HINSTANCE hMod, // handle to application instance

DWORD dwThreadId // thread identifier

);

Ý nghĩa của từng tham số :

idHook : Xác định loại hook mà ta muốn cài đặt, tham số này có thể là một trong các giá trị sau :

· WH_CALLWNDPROC

· WH_CALLWNDPROCRET

· WH_CBT

· WH_DEBUG

· WH_FOREGROUNDIDLE

· WH_GETMESSAGE

· WH_JOURNALPLAYBACK

· WH_JOURNALRECORD

· WH_KEYBOARD

· WH_MOUSE

· WH_MSGFILTER

· WH_SYSMSGFILTER

Mỗi giá trị trên xác định một loại hook mà ta muốn cài đặt, mỗi loại hook có một ý nghĩa và tình huống sử

dụng khác nhau, chi tiết về từng loại hook sẽ được đề cập sau.

lpfn : Địa chỉ của Filter Function mà ta muốn gắn với hook, hàm này phải được “export” bằng macro thích

hợp khi ta cài đặt.

hMod : Handle của module chứa Filter Function. Nếu ta cài đặt một hook cục bộ (nghĩa là sự thực thi của

Filter Function chỉ ảnh hưởng đối với tiến trình cài đặt hook), tham số này phải là NULL. Còn nếu ta muốn

có một hook với phạm vi toàn hệ thống (tức là mọi tiến trình đang hiện hữu đều chịu ảnh hưởng bởi Filter

Function của ta), tham số này sẽ là Handle của DLL chứa Filter Function.

dwThreadID : Định danh của thread ứng với hook đang được cài đặt . Nếu tham số này là một số khác 0,

Filter Function được gắn với hook chỉ được gọi trong ngữ cảnh của một thread xác định. Còn nếu

dwThreadID = 0, Filter Function sẽ có phạm vi toàn hệ thống, và dĩ nhiên, nó sẽ được gọi trong ngữ cảnh

của bất kỳ thread nào đang tồn tại trên HĐH. Có thể sử dụng hàm GetCurrentThreadId để lấy được

handle của thread muốn cài đặt hook.

Một hook có thể được sử dụng ở mức hệ thống, ở mức cục bộ, hoặc ở cả hai mức vừa nêu. Bảng sau mô

tả loại hook cùng tầm ảnh hưởng của nó :

WH_CALLWNDPROC

Thread , global

WH_CALLWNDPROCRET

Thread , global

WH_CBT

Thread , global

WH_DEBUG

Thread , global

WH_FOREGROUNDIDLE

Thread , global

WH_GETMESSAGE

Thread , global

WH_JOURNALPLAYBACK

Global

WH_JOURNALRECORD

Global

WH_KEYBOARD

Thread , global

WH_MOUSE

Thread , global

WH_MSGFILTER

Thread , global

WH_SYSMSGFILTER

Global

Với một loại hook xác định, hook cục bộ sẽ được gọi trước, sau đó là hook toàn cục.

SetWindowsHookEx trả về handle của hook được cài đặt (là 1 giá trị có kiểu HHOOK). Giá trị này cần được

lưu lại để dùng trong hàm UnhookWindowsHookEx khi ta muốn gỡ bỏ hook. Nó sẽ trả về NULL nếu không

thể gắn Filter Function vào hook, trong trường hợp này, hãy gọi hàm GetLastError để biết lý do cài đặt hook

thất bại, giá trị mã lỗi là một trong các giá trị sau :

  • ERROR_INVALID_HOOK_FILTER : hằng số xác định loại hook sai.
  • ERROR_INVALID_FILTER_PROC : địa chỉ Filter Function không chính xác.
  • ERROR_HOOK_NEEDS_HMOD : tham số hMod của hàm SetWindowsHookEx đang là NULL trong khi ứng dụng muốn cài đặt một hook toàn cục.
  • ERROR_GLOBAL_ONLY_HOOK : Ứng dụng đang cố cài đặt một hook cục bộ trong khi bản chất của loại hook này là hook toàn cục.
  • ERROR_INVALID_PARAMETER : giá trị của tham số dwThreadID không chính xác
  • ERROR_JOURNAL_HOOK_SET : Đã có một hook loại “Journal” (WH_JOURNALPLAYBACK hoặc WH_JOURNALRECORD) đang hiện hữu trong hệ thống. Vì lý do an toàn, Windows chỉ cho phép ta cài đặt một và chỉ một hook loại “Journal” tại một thời điểm.
  • ERROR_MOD_NOT_FOUND : Ứng dụng không thể định vị handle của DLL trong khi muốn cài đặt một hook toàn cục.
  • Các giá trị khác : Vấn đề an toàn của HĐH không cho phép hook này được cài đặt, hoặc hệ thống đã hết bộ nhớ.

Gỡ bỏ một Filter Function ra khỏi chuỗi các Filter Function của một hook Windows cung cấp hàm UnhookWindowsHookEx giúp ta thực hiện việc này.Khai báo của nó như sau :

BOOL UnhookWindowsHookEx(

HHOOK hhk // handle to hook procedure

);

Tham số duy nhất của hàm này là handle của hook cần được gỡ bỏ. Giá trị này là giá trị được trả về từ hàm SetWindowsHookEx khi ta cài đặt hook.

Chi tiết về Filter Function

Đây là thời điểm thích hợp để bàn về Filter Function. Nhắc lại rằng Filter Function là một hàm được gắn với loại hook mà ta muốn cài đặt. Hàm này được gọi bởi HĐH Windows chứ không được gọi bởi ứng dụng, đây cũng là lý do mà đôi khi người ta thường gọi nó là “Callback Function”. Tuy nhiên , để thống nhất về mặt thuật ngữ, từ nay về sau ta vẫn gọi nó là Filter Function.

Tất cả các Filter Function đều có dạng sau :

LRESULT CALLBACK FilterFunc(int nCode, WPARAM wParam, LPARAM lParam);

Lưu ý : “FilterFunc” chỉ là tên hàm tượng trưng, khi cài đặt, Filter Function sẽ có tên bất kỳ theo ý của lập trình viên.

· Ý nghĩa của từng tham số truyền cho hàm.

nCode : tham số này thường được gọi là “hook code”, Filter Function sử dụng giá trị này để quyết định cách thức xử lý đối với sự kiện.

(Lưu ý là điều này hoàn toàn tuỳ thuộc vào lập trình viên. Có thể hình dung hook code đem lại những thông tin nào đó, và từ những thông tin này, lập trình viên sẽ quyết định xử lý sự kiện bắt được như thế nào theo hướng riêng của anh ta).

Giá trị của hook code tùy thuộc vào từng loại hook cụ thể, và mỗi loại hook sẽ có tập hợp những giá trị hook code đặc trưng của riêng mình. Có một quy luật mà dường như các Filter Function của mọi loại hook cần tuân thủ : Khi Windows truyền cho hàm giá trị hook code âm, Filter Function không được xử lý sự kiện mà phải gọi hàm CallNextHookEx với chính những tham số mà HĐH truyền cho nó. Sau đó, nó phải trả về giá trị được trả về bởi hàm CallNextHookEx. wParam, lParam : Đây là những thông tin cần thiết cho Filter Function trong quá trình xử lý sự kiện. Các giá trị này sẽ có ý nghĩa khác nhau tuỳ thuộc vào từng loại hook. Ví dụ, Filter Function gắn với hook WH_KEYBOARD sẽ nhận mã phím ảo (Virtual-Key Code) từ wParam, đồng thời có được từ lParam thông tin mô tả trạng thái của bàn phím khi sự kiện gõ phím xảy ra.

· Gọi Filter Function kế tiếp trong chuỗi các Filter Function Khi một hook được cài đặt, Windows gọi hàm đầu tiên trong chuỗi các Filter Function, và kể từ thời điểm này, trách nhiệm Windows không còn nữa. Filter Function hiện hành phải đảm bảo với hệ thống là có được lời gọi đến hàm kế tiếp trong chuỗi các Filter Function. Bởi lẽ, có thể có một ứng dụng khác cũng cài đặt cùng loại hook để thi hành một số tác vụ nào đó, và nếu như ta không cho Filter Function của ứng dụng này tham gia xử lý sự kiện, sẽ có vấn đề rắc rối xảy ra. Vấn đề sẽ càng trở nên nghiêm trọng nếu ứng dụng này là một chương trình thuộc hệ thống, và rõ ràng sẽ không có gì đảm bảo cho sự an toàn của hệ thống chúng ta. Để giải quyết vấn đề trên, hãy sử dụng hàm CallNextHookEx, khai báo của nó như sau :

LRESULT CallNextHookEx(

HHOOK hhk, // handle to current hook

int nCode, // hook code passed to hook procedure

WPARAM wParam, // value passed to hook procedure

LPARAM lParam // value passed to hook procedure

);

Tham số đầu tiên là handle của hook hiện hành, giá trị này có thể lấy được từ hàm SetWindowsHookEx

khi cài đặt hook. Ba tham số tiếp theo là những giá trị mà Windows truyền cho Filter Function.Trong một

số tình huống, Filter Function hiện hành có thể không muốn chuyển sự kiện cho Filter Function khác

trong cùng một chuỗi. Lúc này, nếu loại hook ta đang cài đặt cho phép huỷ bỏ sự kiện, và Filter Function

của ta cũng có cùng quyết định là hủy bỏ, nó sẽ không phải gọi hàm CallNextHookEx.

Có một thực tế mà các lập trình viên buộc phải chấp nhận : họ sẽ không thể biết được vị trí Filter Function

của mình trong chuỗi các Filter Function. Bởi vì khi ta gọi hàm CallNextHookEx, Windows mới là đối

tượng quyết định xem phải chọn hàm nào để bàn giao sự kiện, và quá trình này dĩ nhiên đã được HĐH

giấu kín. Do đó, một cách khôn ngoan (và bắt buộc) để đảm bảo an toàn hệ thống là tuân thủ các nguyên

tắc của HĐH khi cài đặt và sử dụng một hook.

Filter Function cho Hook toàn cục và DLL Như đã có lần đề cập, khi ta muốn cài đặt một hook toàn cục,

Filter Function phải là một hàm nằm trong một DLL (Dynamic Link Library). Tuy nhiên, vẫn có một ngoại

lệ khi ta cài đặt hai hook toàn cục WH_JOURNALRECORD và WH_JOURNALPLAYBACK. Bởi lẽ đối với

hai loại hook này, Windows gọi Filter Function theo một cách đặc thù, nên hàm này không cần phải nằm

trong DLL.

III.4. Giới thiệu hook WH_KEYBOARD và WH_GETMESSAGE

Bây giờ sẽ là chi tiết của Filter Function gắn với hook WH_KEYBOARD WH_GETMESSAGE, hai loại hook liên quan có thể được chọn để cài đặt . Filter Function cho hook WH_KEYBOARD

Windows gọi Filter Function của hook này khi GetMessage hoặc PeekMessage trả về các thông điệp WM_KEYDOWN, WM_KEYUP. Prototype của hàm này có dạng như sau :

LRESULT CALLBACK KeyboardProc(

int code, // hook code

WPARAM wParam, // virtual-key code

LPARAM lParam // keystroke-message information );

code : xác định cách thức Filter Function xử lý thông điệp, nó là một trong hai giá trị sau :

Value

Meaning

HC_ACTION

Thông điệp đã được lấy ra khỏi hàng đợi, wParamlParam chứa thông tin về thông điệp mà Filter Function nhận được.

HC_NOREMOVE

wParam lParam mang thông tin của thông điệp, và thông điệp vẫn còn nằm trong hàng đợi (Ứng dụng gọi PeekMessage với tuỳ chọn PM_NOREMOVE).

Nếu code là giá trị âm, Filter Function phải chuyển thông điệp cho Filter Function khác bằng cách gọi CallNextHookEx, rồi trả về giá trị được trả về bởi hàm CallNextHookEx.

wParam : chứa mã phím ảo của phím được nhấn.

lParam : các thông tin khác về sự kiện gõ phím, các thông tin này đã được mô tả ở các phần trên.

Filter Function cho hook WH_GETMESSAGE

Như phần đầu đã giới thiệu, trước khi GetMessagePeekMessage trả thông điệp về cho ứng dụng, thông điệp sẽ được chuyển cho Filter Function của hook để hàm này có thể thực hiện một số thao tác kiểm tra, chỉnh sửa thông tin của message. Tuy nhiên, Filter Function không thể hủy bỏ thông điệp mà nó nhận được. Đây là nguyên tắc cơ bản khi sử dụng hook WH_GETMESSAGE. Filter Function của hook này có dạng sau :

LRESULT CALLBACK GetMsgProc(

int code, // hook code

WPARAM wParam, // removal option

LPARAM lParam // message

);

code : Filter Function sử dụng giá trị này để quyết định xem mình có phải xử lý message hay không. Nếu code=HC_ACTION, Filter Function phải xử lý thông điệp. Nếu code là một giá trị âm, Filter Function phải chuyển message cho hàm khác bằng cách gọi CallNextHookEx mà không có bất cứ sự thay đổi nào trên thông điệp. Sau đó, nó phải trả về giá trị được trả về bởi hàm CallNextHookEx.

wParam : giá trị tham số này cho ta biết message mà Filter Function nhận được có được lấy ra khỏi hàng đợi hay chưa. Nó có thể là một trong các giá trị sau

Giá trị

Ý nghĩa

PM_NOREMOVE

Message vẫn còn nằm trong hàng đợi, điều này đồng nghĩa với việc ứng dụng gọi PeekMessege với tuỳ chọn PM_NOREMOVE .

PM_REMOVE

Message đã được lấy ra khỏi hàng đợi, ứng dụng đã gọi

GetMessage, hoặc PeekMessage với tuỳ chọn PM_REMOVE.

lParam : trỏ đến cấu trúc MSG chứa thông tin chi tiết về message.

IV.Cài đặt và sử dụng một hook toàn cục

Phần này sẽ nêu vắn tắt cách cài đặt và sử dụng một hook toàn cục, chúng ta sẽ xét một bài toán đơn giản sau làm ví dụ minh họa :

Giả sử ta đang soạn thảo văn bản trong một môi trường nào đó (Notepad, Microsoft Word,...), hãy viết một chương trình theo dõi các phím được gõ của người dùng, nếu là phím ‘0’ ở góc phải bàn phím thì trên màn hình soạn thảo sẽ xuất hiện thêm 2 ký tự ‘d’ và ‘t’.

Phân tích sự kiện để xác định loại hook phù hợp

Theo phát biểu của bài toán trên, ta nghĩ ngay đến hook WH_KEYBOARD, bên cạnh đó cũng có một hook có thể làm nên chuyện, đấy chính là hook WH_GETMESSAGE (nên nhớ rằng Filter Function ứng với loại hook này có thể hứng mọi loại thông điệp trả về từ GetMessage hoặc PeekMessage).

Hướng tiếp cận dễ nhận thấy để giải quyết vấn đề là chặn thông điệp WM_KEYDOWN hoặc WM_KEYUP (không cần chặn cả hai), kiểm tra là phím gì, nếu là phím ‘0’ của bàn phím số thì dùng hàm keybd_event để gởi phím ‘d’ và ‘t’.

Nếu ta dùng hook WH_KEYBOARD, Filter Function của nó sẽ được gọi bất cứ khi nào xuất hiện message WM_KEYDOWN hoặc WM_KEYUP. Kết quả là nếu ta không phân biệt rõ hai thông điệp này, Filter Function sẽ được gọi 2 lần chỉ với một động tácgõ phím. Mặt khác để phân biệt 2 message này cần phải thực hiện các phép toán trên bit đối với giá trị lParam, đồng thời phải nhớ sự khác nhau giữa lParam của WM_KEYDOWN với lParam của WM_KEYUP. Điều này tương đối là phức tạp.

Còn nếu ta dùng hook WH_GETMESSAGE, vấn đề sẽ đơn giản hơn, bởi việc phân biệt hai message trên sẽ được thực hiện bằng chính tên của hai thông điệp đó. Hãy nhớ rằng đối với loại hook này, lParam của Filter Function trỏ đến cấu trúc MSG của thông điệp hứng được.

Với lý giải vừa nêu, chúng ta sẽ chọn hook WH_GETMESSAGE.

Tiến hành cài đặt DLL chứa Filter Function của hook

Đây là hook toàn cục, do đó Filter Function phải là một hàm được “export” của một DLL nào đó. Đầu tiên, ta định nghĩa chỉ thị để “export” một hàm :

#define EXPORT __declspec( dllexport)

Từ đây, muốn “export” hàm nào, ta chỉ việc dùng lệnh : EXPORT Tên_Hàm(Tham_số_1, Tham_số_2,...)

Trong DLL sẽ có 3 hàm chính cần phải được “export” :

· Hàm cài đặt hook

· Hàm gỡ bỏ hook

· Filter Function của hook

Hai hàm đầu được ứng dụng cài đặt hook gọi, còn Filter Function dĩ nhiên sẽ được gọi bởi HĐH Windows. Và sau đây là một phiên bản DLL khả dĩ cho bài toán trên :

#include "StdAfx.h"

// Định nghĩa chỉ thị EXPORT

#define EXPORT __declspec(dllexport)

// Export 2 hàm cài đặt, gở bỏ hook

EXPORT void WINAPI InstallHook();

EXPORT void WINAPI RemoveHook();

// Export Filter Function

EXPORT LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lPram);

// Handle của DLL

HANDLE hDllInst = NULL;

// Handle của hook được cài đặt

HHOOK hGetMsgHook = NULL;

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)

{

// Lấy handle của DLL

hDllInst = hModule;

return TRUE;

}

/* Giả lập nhấn phím (Release=FALSE) hay nhả phím (Release = TRUE) phím

có mã phím ảo vk */

void SendKey(BYTE vk, BOOL Release=FALSE)

{

if (Release)

keybd_event(vk, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);

else

keybd_event(vk, 0, KEYEVENTF_EXTENDEDKEY, 0);

}

// Cài đặt hook

EXPORT void WINAPI InstallHook()

{ if (hGetMsgHook == NULL)

hGetMsgHook = SetWindowsHookEx(WH_GETMESSAGE,

(HOOKPROC)GetMsgProc, (HINSTANCE)hDllInst, 0);

}

// Gỡ bỏ hook

EXPORT void WINAPI RemoveHook()

{

if (hGetMsgHook != NULL)

{

UnhookWindowsHookEx(hGetMsgHook);

hGetMsgHook = NULL;

}

}

// Filter Function cho hook

EXPORT LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)

{

MSG* p = (MSG*)lParam;

// Chỉ xử lý khi message đã được lấy ra khỏi hàng đợi

if ( (nCode >= 0) && (wParam == PM_REMOVE) )

{

// Thông điệp là WM_KEYDOWN

if (p->message == WM_KEYDOWN)

// Phim được nhấn là phím ‘0’ ở góc phải bàn phím

if (p->wParam == VK_NUMPAD0)

{

SendKey(0x44); // Gởi phím ‘d’

SendKey(0x54); // Gởi phím ‘t’

}

}

// Nếu nCode<0>

// kế tiếp

return (CallNextHookEx(hGetMsgHook, nCode, wParam, lParam));

}

Sử dụng DLL và hoàn chỉnh chương trình

Đầu tiên là định nghĩa chỉ thị IMPORT để “import” một hàm trong DLL dùng cho ứng dụng.

#define IMPORT __declspec( dllimport )

Sau đó dùng chỉ thị này để “import” hai hàm : InstallHook RemoveHook.

Việc sử dụng hook bây giờ sẽ cực kỳ đơn giản, ta chỉ cần vào hàm xử lý message cho cửa sổ của ứng dụng, chặn message WM_CREATE và WM_DESTROY như đoạn code sau minh họa :

case WM_CREATE :

InstallHook();

// Các xử lý khác ...

break;

case WM_DESTROY :

RemoveHook();

// Các xử lý khác ...

break;

Sau khi cài đặt hook, việc gọi Filter Function là trách nhiệm của Windows, nhiệm vụ duy nhất của ta từ lúc này là chờ người dùng kết thúc ứng dụng thì gỡ bỏ hook.

Đến đây xem như ta đã giải quyết được bài toán đặt ra. Qua ví dụ này, thiết nghĩ việc cài đặt một hook xem ra không phải là quá khó. Mấu chốt của vấn đề là phải chọn được loại hook phù hợp và nắm các thông tin liên quan đến sự kiện ta muốn chặn một cách đầy đủ, chính xác

Source: http://ity.vnuit.edu.vn/congnghe/000009/index.htm