Phí Văn Ngọc
1. Đặt vấn đề
Với các sản phẩm phần mềm bảo mật như Personal Firewall thì việc giám sát việc tạo,
hủy tiến trình là rất quan trọng trong việc phát hiện và ngăn chặn các tiến trình “độc” thực thi
mã của chúng. Tuy nhiên, các tiến trình được khởi tạo và quản lý bởi hệ điều hành. Chương
trình của người sử dụng ở tầng User-mode không thể can thiệp vào quá trình này. Do vậy, để
có thể đạt được một kết quả khả quan, cần phải có những cơ chế tốt cho phép can thiệp vào
nhân hệ điều hành và đón bắt được cách thức hệ điều hành tạo ra cũng như quản lý các tiến
trình.
2. Các nhiệm vụ chính cần giải quyết
Báo cáo này sẽ chỉ tập trung giải quyết các vấn đề chủ yếu sau:
• Đón bắt việc tạo và hủy tiến trình, tiểu trình, nạp thư viện DLL một cách Real-time
• Đón bắt việc một tiến trình chuẩn bị được tạo ra và can thiệp được vào quá trình
này.
3. Đón bắt việc tạo, hủy tiến trình
Trên Windows, các ứng dụng ở tầng User mode thường sử dụng hàm API
CreateProcess() để tạo và thực thi một tiến trình mới. Hàm API này thực chất chỉ là một hàm
trung gian chuyển lời gọi từ User mode đến hàm thực sự tạo tiến trình trong Kernel mode.
Trong Kernel mode, tồn tại các hàm API có tên dạng tương tự như các hàm API trong User
mode. Các hàm này được gọi là các hàm native API – NT. Ví dụ, hàm NT tương ứng của
CreateProcess() là NtCreateProcess().
Windows NT trợ giúp nhiều hệ thống con (subsystem) bao gồm :Win32, POSIX và
OS2. Mỗi subsystem bao gồm một tập các hàm API mà người dùng ở User mode có thể gọi để
thực hiện các tác vụ mong muốn. Chẳng hạn, với Win32 subsystem, thư viện Kernel32.dll
cung cấp rất nhiều các hàm để thao tác với tiến trình, bộ nhớ, vào ra…Tuy nhiên, Kernel32.dll
thực chất không phải là nhân của hệ điều hành. Nó chỉ là một lớp trung gian làm nhiệm vụ
giao tiếp giữa lời gọi từ User mode vào Kernel mode. Việc chuyển lời gọi hàm này cuối cùng
sẽ được thực hiện bởi các hàm trong thư viện Ntdll.dll. Hình vẽ sau minh họa quá trình route
một lời gọi hàm từ User mode vào Kernel mode:
Hình ảnh này đã được thu nhỏ. Click vào đây để xem hình gốc. Kích thước của hình gốc là 707x435. |
Thư viện Ntdll.dll có thể được coi là “lá chắn” cuối cùng của tầng User mode tiếp giáp
với Kernel mode. Các đoạn mã thực sự thi hành các dịch vụ của hệ điều hành nằm trong file
ntoskrnl.exe. Ntdll.dll chỉ làm nhiệm vụ phơi bày một số hàm API mà ntoskrnl.exe cung cấp
để cho Kernel32.dll, User32.dll, Gdi32.dll…gọi chúng.
Hiểu được cơ chế ở trên, để có thể đón bắt việc tạo và hủy tiến trình một cách realtime,
ta đề xuất một số giải pháp sau:
3.1. Giải pháp 1: sử dụng API hooking trong User mode
API hooking cho phép hook một hàm API mà một thư viện nào đó đã export. Để có thể
hook, cần phải đặt mã của hàm hook vào trong một file DLL. Sau đó, nạp file DLL này vào
tiến trình cần hook. Tiến trình cần hook chính là tiến trình đã nạp thư viện dll đã export hàm
API trên. Có nhiều cách thực hiện việc hook API ở tầng user mode mà điển hình nhất là sử
dụng Trampoline function (thư viện Detours đã sử dụng kĩ thuật này).
Với mục đích đã đặt ra, các hàm cần hook bao gồm: CreateProcess(),WinExec(),
ShellExecute(), Yield(), TerminateProcess(), ExitProcess(). Hơn nữa, cần phải hook mọi tiến
trình đang chạy nhằm phát hiện việc tạo, hủy tiến trình từ mọi tiến trình này. Điều này là
không tối ưu nếu xét về góc độ hiệu năng và tính hiệu quả. Thứ nhất, việc sử dụng API hook
cho toàn bộ tiến trình sẽ làm suy giảm hiệu năng hệ thống. Thứ hai, có thể tồn tại các hàm API
khác dạng undocumented cũng có chức năng tạo tiến trình. Khi đó, việc hook này sẽ không
thành công.
3.2. Giải pháp 2: Sử dụng Polling kết hợp với các hàm API liệt kê tiến
trình
Theo phương pháp này, một Thread ngầm sẽ được tạo ra. Nhiệm vụ của Thread này là
liên tục capture các tiến trình của hệ điều hành nhằm phát hiện việc tạo và hủy tiến trình mới.
Để capture thông tin về tiến trình, có thể sử dụng các hàm API do các thư viện
psapi.dll, thư viện Tool help 32 như : EnumProcess(), CreateToolhelp32Snapshot()… Ưu điểm
của phương pháp này là dễ cài đặt. Tuy nhiên, không tối ưu về hiệu năng cũng như tính realtime.
3.3. Giải pháp 3: Sử dụng API hooking trong Kernel mode
Như đã đề cập ở trên, các hàm API trong User mode chỉ thực sự là trung gian chuyển
lời gọi từ User mode vào Kernel mode. Chẳng hạn, CreateProcess() thực chất sẽ gọi vào
NtCreateProcess(). Do vậy, để tăng tính triệt để và thu hẹp phạm vi kiểm soát, có thể sử dụng
phương pháp Hook các hàm native API.
Theo phương pháp này, hàm hook sẽ được đặt trong một Device Driver. Vai trò của
Device Driver trong Kernel mode cũng tương tư như các file DLL trong User mode khi sử
dụng System-wide hook. Các hàm native API liên quan đến tạo, hủy tiến trình sẽ bị hook.
Hàm hook sẽ đón bắt việc gọi đến các hàm này và trả về thông tin cho người dùng ở User
mode đồng thời gọi hàm bị hook (e.g, NtCreateProcess) để tiến trình có thể tạo và hủy một
cách hợp lệ.
Ưu điểm của phương pháp này là có thể thu hẹp phạm vi giám sát các hàm API. Thay
vì phải giám sát một số lượng lớn các hàm API tạo, hủy tiến trình ở User mode, phương pháp
này chỉ giám sát một số hàm native API ở Kernel mode. Do vậy, sẽ có thể đón bắt tốt hơn việc
tạo, hủy tiến trình cũng như tăng tính Real-time.
Nhược điểm của phương pháp này là nếu cài đặt không tốt, có thể gây ra sự phụ thuộc
qúa nhiều vào các hàm native API vốn có thể bị thay đổi bởi Microsoft trong các phiên bản
khác nhau của Windows.
3.4. Giải pháp 4: Sử dụng native API tiện ích của hệ điều hành
Trong tất cả các giải pháp đưa ra, có lẽ giải pháp này sẽ mang lại hiệu quả hơn cả.
Nguyên lý của giải pháp này dựa trên cơ chế sau: Windows cung cấp một số hàm API ở tầng
Kernel mode hỗ trợ việc đăng ký một hàm Callback nhằm thông báo việc tạo và hủy tiến trình.
Mỗi khi có một tiến trình được tạo hoặc hủy, Windows sẽ gọi hàm Callback này. Hàm
Callback cần phải được đặt trong một Device Driver vì cơ chế này chỉ làm việc ở Kernel
mode.
Hàm mà ta vừa nêu có dạng như sau:
Tham số Remove sẽ thiết lập cơ chế Callback (FALSE) hoặc loại bỏ hàm
Callback(TRUE).
Hàm Callback có dạng sau:
Trong hàm Callback này, các ham số bao gồm:
- Code: Chọn hết
NTSTATUS PsSetCreateProcessNotifyRoutine(IN
PCREATE_PROCESS_NOTIFY_ROUTINE Callback,IN BOOLEAN
Remove);
VOID ProcessCallback(IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create);
• ParentId: ID của Process cha đã tạo hoặc hủy tiến trình
• ProcessId: ID của Process được tạo hoặc hủy
• Create: TRUE nếu Process được tạo, FALSE nếu bị hủy
Trong hàm Callback, các thông tin về tiến trình vừa tạo hoặc hủy sẽ được thông báo
cho chương trình người sử dụng ở User mode biết và xử lý. Ở đây, ta sử dụng cơ chế giao tiếp
sự kiện. Một đối tượng đồng bộ kiểu Event được tạo ra trong DriverEntry. Chương trình User
mode sẽ lắng nghe Event này. Khi hàm Callback được gọi, Event được thiết lập trạng thái
thành Signaled và chương trình User mode sẽ được thông báo để có thể gửi lệnh IOCTL vào
Driver và lấy thông tin cần thiết. Thông tin về tiến trình sẽ được lưu tạm thời trong một cấu
trúc PROCESS_CALLBACK_INFO của DEVICE_EXTENSION.
- Code: Chọn hết
typedef struct tagProcessCallbackInfo
{
HANDLE hParentID;
HANDLE hProcessID;
BOOLEAN bCreated;
}PROCESS_CALLBACK_INFO,*PPROCESS_CALLBACK_INFO;
//....................
/***********************************************/
void ProcessCallback(IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create)
{
PDEVICE_EXTENSION pDevExtension;
// Lấy về đối tượng Device Extension từ biến toàn
cục g_pDeviceObject.
pDevExtension=(PDEVICE_EXTENSION)g_pDeviceObject-
>DeviceExtension;
// Gán thông tin tiến trình vào cấu trúc ProcessInfo
trong Device Extension
pDevExtension->ProcessInfo.hParentID=ParentId;
pDevExtension->ProcessInfo.hProcessID=ProcessId;
pDevExtension->ProcessInfo.bCreated=Create;
// Thiết lập Event để thông báo cho User mode
KeSetEvent( pDevExtension->KeProcessEvent,
0,
FALSE);
KeClearEvent(pDevExtension->KeProcessEvent);
}
// Lấy về thông tin tiến trình qua IOCTL
NTSTATUS DispatchIoctl(IN PDEVICE_OBJECT DeviceObject, IN PIRP
Irp)
{
//.................
switch(pIoStack->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_GET_PROCESS_INFO:
{
if(pIoStack-
>Parameters.DeviceIoControl.OutputBufferLength>=
sizeof(PROCESS_CALLBACK_INFO))
{
// Set user-supplied buffer
pProcessInfo=(PPROCESS_CALLBACK_INFO)Irp-
>AssociatedIrp.SystemBuffer;
// Lấy thông tin tiến trình đã lưu trong
Device Extension và gán vào bộ đệm trả về cho User mode
pProcessInfo->hParentID=pDevExtension-
>ProcessInfo.hParentID;
pProcessInfo->hProcessID=pDevExtension-
>ProcessInfo.hProcessID;
pProcessInfo->bCreated=pDevExtension-
>ProcessInfo.bCreated;
}
break;
}
4. Đón bắt việc tạo, hủy tiểu trình và các module
Cơ chế tương tự cũng được sử dụng để đón bắt việc tạo, hủy các tiểu trình (Thread) và
nạp, hủy nạp các module DLL. Các hàm sử dụng cho việc này như sau:
- Code: Chọn hết
NTSTATUS
PsSetCreateThreadNotifyRoutine(PCREATE_THREAD_NOTIFY_ROUT
INE Callback;
NTSTATUS
PsSetLoadImageNotifyRoutine(PLOAD_IMAGE_NOTIFY_ROUTINE
Callback);
VOID ThreadCallback( IN HANDLE ProcessId,
IN HANDLE ThreadId,
IN BOOLEAN Create);
VOID ImageCallback( IN PUNICODE_STRING FullImageName,
IN HANDLE ProcessId,
IN PIMAGE_INFO ImageInfo);
5. Đón bắt và can thiệp quá trình tạo tiến trình
Như đã phân tích ở trên, kĩ thuật hook các hàm native API tỏ ra có lợi thế trong các
trường hợp này. Để đón bắt việc tạo một tiến trình và có thể can thiệp nó (hủy không cho tạo
hoặc cho tiếp tục tạo tiến trình), cần phải có cơ chế hook các hàm native API có khả năng tạo
tiến trình. Một ứng cử viên tiềm tàng là hàm NtCreateProcess(). Tuy nhiên, không có điều gì
đảm bảo rằng có hay không một hàm khác cũng có thể tạo tiến trình. Vì vậy, cần phải hook ở
mức độ hẹp hơn. Cơ chế tạo tiến trình có thể được minh họa bởi dãy các hàm sau:
Hình ảnh này đã được thu nhỏ. Click vào đây để xem hình gốc. Kích thước của hình gốc là 959x76. |
Do vậy, để có thể đón bắt ở mức hẹp hơn, ta nên hook hàm NtCreateSection(). Hàm
này sẽ được gọi trước khi hàm NtCreateProcess() được gọi. Việc hook hàm NtCreateSection()
được thực hiện bằng cách can thiệp vào bảng System Service Dispatch Table (bảng chứa
vector đến địa chỉ các hàm thực sự cung cấp dịch vụ của hệ điều hành) và sửa đổi địa chỉ của
hàm NtCreateSection() thành địa chỉ của hàm hook đặt trong Driver. Cấu trúc của bảng SSDT
như sau:
Hình ảnh này đã được thu nhỏ. Click vào đây để xem hình gốc. Kích thước của hình gốc là 688x397. |
Để tiện lợi, ta định nghĩa một macro dùng cho việc lấy về địa chỉ của một hàm native
API bất kỳ như sau:
NtCreateSection() NtCreateProcess() NtCreateThread() NtCreateFile()
Macro trên sẽ lấy về địa chỉ hàm _function.
Các thủ tục Hook và UnHook đơn giản là thay thế địa chỉ hàm thực bởi hàm hook và
ngược lại:
RealZwCreateSection là một con trỏ hàm đến hàm NtCreateSection.
Trong hàm hook, một đối tượng Event sẽ được thiết lập trạng thái signaled để thông
báo cho người sử dụng và đợi ý kiến của người dùng (cho phép hoặc cấm) tiến trình thực hiện.
Tùy theô kết quả trả về mà hàm hook sẽ gọi hàm gốc (cho phép) hoặc trả về
STATUS_ACCESS_DENIED để báo rằng việc tạo tiến trình không được thực hiện.
- Code: Chọn hết
#define SYSCALL(_function) ServiceTable->ServiceTable[
*(PULONG)((PUCHAR)_function+1)]
NTSTATUS (*RealZwCreateSection) ( OUT PHANDLE,
IN ULONG,
IN POBJECT_ATTRIBUTES,
IN PLARGE_INTEGER,
IN ULONG,
IN ULONG,
IN HANDLE
);
void SetHook(){
// Lưu hàm gốc
RealZwCreateSection=SYSCALL(ZwCreateSection);
// Thay hàm gốc bằng hàm hook
SYSCALL(ZwCreateSection)=(PVOID)HookZwCreateSection;
}
void UnsetHook(){
// Khôi phục hàm gốc
SYSCALL(ZwCreateSection)=(PVOID)RealZwCreateSection;
}
NTSTATUS HookZwCreateSection(OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributess,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL )
{
//........................
// Set event so that user can be notified
KeSetEvent(pDevExt->KeHookEvent,
0,
FALSE);
KeClearEvent(pDevExt->KeHookEvent);
DbgPrint("Waiting for user response...\n");
// Đợi trả lời của người dùng
UserResponse.bResponsed=FALSE;
while(1)
{
KeDelayExecutionThread(KernelMode,0,&li);
if(UserResponse.bResponsed)
break;
}
// Nếu người dùng cho phép thì gọi hàm gốc
if(UserResponse.bAllowed)
{
DbgPrint("ProcMan: User responsed allow\n");
RETURN_ORG;
}
// Nếu không thi trả về lỗi và hủy việc tạo tiến trình
else
{
DbgPrint("ProcMan: User responsed disallow\n");
return STATUS_ACCESS_DENIED;
}
return STATUS_SUCCESS;
6. Chương trình Demo
Chương trình demo được xây dựng nhằm sử dụng Driver đã tạo và minh họa kết quả.
Chương trình được viết bằng Visual C++ 6.0, sử dụng MFC. Một số kết quả demo như hình
chụp dưới đây:
Hình ảnh này đã được thu nhỏ. Click vào đây để xem hình gốc. Kích thước của hình gốc là 885x639. |
Hình 1: Giao diện chính
Hình 2: Thông báo tiến trình vừa được tạo
Hình 3:Thông báo tiến tình bị hủy
Hình ảnh này đã được thu nhỏ. Click vào đây để xem hình gốc. Kích thước của hình gốc là 800x539. |
Hình 4: Hỏi người dùng quyết định việc có tạo tiến tình hay không
Hình ảnh này đã được thu nhỏ. Click vào đây để xem hình gốc. Kích thước của hình gốc là 652x200. |
Hình 5: Không cho phép tạo tiến trình
Hình ảnh này đã được thu nhỏ. Click vào đây để xem hình gốc. Kích thước của hình gốc là 798x533. |
Hình 6: Ứng dụng xây dựng chức năng lọc chương trình dựa trên tập luật
Hình 7: Tạo luật lọc tiến trình
7. Đánh giá kết quả
Chương trình đã được thử nghiệm trên Windows XP SP2, Windows Server 2003
Standard Edition và cho kết quả tốt. Có thể bắt được việc tạo tiến trình của bất kỳ tiến trình
nào kể cả explorer.exe, task manager… Hiệu năng của hệ thống hầu như ổn định.
Hướng phát triển tiếp theo sẽ bao gồm việc theo dõi các hành vi khác của tiến trình
như: truy cập file, truy cập Registry, truy cập network…
Tài liệu tham khảo
[1] - [Addison Wesley] Undocumented Windows 2000 Secrets - The Programmers Cookbook
[2] – http://www.CodeProject.Com
[3] - MSDN
[4] - Windows 2000 Device Driver Book A Guide For Programmers
[5] – http://www.Rootkit.com