Trao đổi với tôi

http://www.buidao.com

7/20/09

[System Info] Peering into WinNT Native API [1]

Peering into WinNT Native API [1]

Link: http://diendantinhoc.org/forum/post/1023461890/Re-Peering-into-WinNT-Native-API-3.html;jsessionid=E448FA0803A9856EF58E82D7763F2A7C?zone=1


1. Giới thiệu
Những ai làm việc với WinNT hẳn đã từng nghe nói rằng có một API ẩn được NT sử dụng ngầm bên trong. API này, được gọi là Native API, hầu như bị che giấu và chỉ được công bố rộng rãi 1 tập nhỏ. Sự bí ẩn này khiến người ta tin rằng Native API có thể cung cấp 1 sức mạnh kì diệu cho ứng dụng, thậm chí còn cho phép chúng vượt qua các giới hạn an ninh được tạo ra bởi các API chuẩn như Win32. 
Việc Native API chưa bao giờ được Microsoft công bố rõ ràng khiến NT là hệ điều hành thương mại còn lại duy nhất trên thế giới có tập các dịch vụ hệ thống bản địa không được công bố. Nhưng cũng phải nói rằng cho tới giờ phút này, NT API vẫn chưa gây ra vấn đề gì: các chương trình vẫn làm được mọi thứ có thể bằng giao diện do các Hệ thống con môi trường Hệ điều hành (OS Environment subsystem) cung cấp. Tuy nhiên, tìm hiểu về giao diện bản địa của HĐH sẽ giúp ta rõ hơn về cách thức hệ thống làm việc. Hơn nữa, một cái nhìn bao quát về native API sẽ giúp chúng ta xóa bỏ những hiểu sai (nếu có) về việc Native API được sử dụng như thế nào, tại sao chúng được sử dụng, và những API không công bố che giấu ta những gì.
Để vấn đề được trình bày rõ hơn, tôi xin mô tả sơ qua kiến trúc WinNT trước khi giới thiệu Native API là gì, nó được gọi trong 1 hoạt động thông thường như thế nào và vì sao nó được sử dụng làm cơ sở hạ tầng hỗ trợ API của các Hệ thống con môi trường HĐH. Tiếp đó tôi sẽ liệt kê một số hàm Native API đã được khám phá cùng cách xây dựng 1 ứng dụng sử dụng chúng.
2. Tổng quan kiến trúc WinNT
Việc tìm hiểu cặn kẽ kiến trúc WinNT vượt quá phạm vi bài viết nho nhỏ này, và thật sự phải được chứa trong hàng đống sách. Phần này chỉ miêu tả kiến trúc WinNT theo quan điểm biểu đồ khối mức cao và cố gắng đưa ra 1 cách tổng quát kiến trúc WinNT nhằm làm rõ hơn cho vấn đề sẽ được trình bày ở phần tiếp theo. Nếu bạn đã hiểu về tổng quan kiến trúc WinNT thì có thể bỏ qua. 8))
Lịch sử phát trển máy tính chứng kiến sự ra đời của rất nhiều kiểu kiến trúc HĐH: từ monolithique (DOS) đến phân lớp (UNIX), Client/Server v.v… [1]. Kiến trúc hệ thống của WinNT là lai của các kiến trúc client/server, phân lớp, hướng đối tượng, và đa xử lí. [3]
 

Hình 1. Kiến trúc cơ sở Windows 2000 — WinNT 5

WinNT, trước tiên, là 1 hệ phân lớp: nó được phân ra làm 2 hệ hoạt động: Kernel-mode và User-mode – cụ thể như sau:
Kernel-Mode
Trong hệ này, chương trình có thể truy nhập trực tiếp vào phần cứng, dữ liệu hệ thống cũng như các tài nguyên hệ thống khác. Kernel-mode bao gồm các thành phần:
- Executive: Chứa các thành phần thực hiện việc quản lí bộ nhớ, quản lí tiến trình và luồng, an ninh, nhập/xuất, giao tiếp liên tiến trình và các dịch vụ HĐH cơ sở khác
- Microkernel: Có nhiệm vụ hàng đầu là cung cấp đồng bộ hóa giữa nhiều bộ VXL, điều phối và phân phối luồng và ngắt, lấy thông tin từ Registry khi hệ thống khởi động…
- Hardware Abstraction Layer (HAL): HAL thay đổi tùy theo phần cứng hệ ĐH đang chạy, vì thế tương thích với nhiều họ VXL. HAL quản lí phần cứng trực tiếp
- Device drivers: Các trình điều khiển thiết bị
- Windowing and graphics system: Thực hiện Giao diện người dùng đồ họa (GUI)
User-Mode
Các chương trình chạy ở user-mode không thể truy nhập phần cứng trực tiếp. Hệ thống con user-mode được bảo vệ và có 4 chức năng chính:
- Các tiến trình hỗ trợ hệ thống đặc biệt, ví dụ tiến trình logon và session manager.
- Các dịch vụ là các tiến trình server, ví dụ các dịch vụ Event Log và Schedule
- Các hệ thống con môi trường cung cấp môi trường HĐH qua các dịch vụ hệ thống bản địa (native system service). Chúng bao gồm các hệ thống con Win32, POSIX, và OS/2.
- Các ứng dụng người dùng – bao gồm Win32, Windows 3.1, MS-DOS, POSIX, hay OS/2.
Các ứng dụng không gọi trực tiếp các dịch vụ hệ thống bản địa của WinNT; thay vì thế, chúng truyền qua các thư viện liên kết động (DLL) của hệ thống con. Đến lượt, các thư viện này dịch hàm được công bố thành các lời gọi dịch vụ hệ thống không được công bố của WinNT – các native WinNT API
to be continued...
3. Cơ chế hoạt động của NT API
Bất cứ khi nào 1 chương trình chạy ở user-mode thực hiện I/O, cấp phát hay hủy bỏ bộ nhớ, khởi tạo tiến trình/luồng hay tương tác với tài nguyên toàn cục... nó phải gọi 1 hay nhiều các dịch vụ nằm trong kernel-mode. Trong WinNT, các dịch vụ này bị che dấu khỏi LTV qua API của các Hệ thống con môi trường, chỉ được thể hiện từ module kernel-mode ntoskrnl.exe tới các ứng dụng user-mode qua 1 thành phần hệ thống duy nhất, đó là ntdll.dll
Trong các Hệ thống con môi trường được WinNT hỗ trợ (Win32, OS/2, POSIX), Win32 được coi là “ngôn ngữ quốc gia”. Hệ thống con môi trường HĐH Win32 được chia thành các tiến trình server (CSRSS.EXE - Client/Server Runtime SubSystem) và các client-side DLL, được kết nối tới các ứng dụng sử dụng Win32 API. Nòng cốt của Win32 API được chia thành 3 loại: “cửa sổ và thông điệp”, “đồ họa”, và “các dịch vụ cơ sở”. Các API “cửa sổ và thông điệp“, bao gồm CreateWindow() và SendMessage(), được xuất tới ứng dụng Win32 qua thư viện USER32.DLL. BitBlt() và LineTo() là các hàm “đồ họa” Win32 và được cung cấp trong GDI32.DLL. Cuối cùng, các dịch vụ cơ sở bao gồm các hàm API thực hiện I/O, quản lí tiến trình và luồng, quản lí bộ nhớ, đồng bộ hóa được thực hiện qua KERNEL32.DLL.
Khi ứng dụng Win32 gọi 1 hàm API, điều khiển được truyền bên trong không gian địa chỉ của nó tới 1 trong những client-side DLL. DLL này – sau khi kiểm tra các thông tin trên tiến trình gọi – sẽ thực hiện 1 trong các lựa chọn sau đây:
• Trả về ngay lập tức 
• Gửi 1 thông điệp tới Win32 server yêu cầu giúp đỡ
• Kích hoạt Native API để thực hiện nhiệm vụ 
Lựa chọn dầu tiên hiếm khi xảy ra, chỉ khi DLL có thể thực hiện nhiệm vụ mà không cần tới các dịch vụ hệ thống của HĐH, 1 ví dụ là hàm GetCurrentProcess().
Lựa chọn thứ 2 cũng hiếm khi xảy ra. Chỉ khi nào server có thể nhận biết và cùng tham gia thực thi nhiệm vụ. Ví dụ như trong trường hợp CreateProcess(), được xuất bởi KERNEL32, Win32 server sẽ gọi các hàm Native API để tạo 1 tiến trình thực sự và chuẩn bị không gian địa chỉ cho client.
Lựa chọn cuối cùng thường xuyên xảy ra nhất. Trước tiên ta hãy xem xét trường hợp các API USER32 và GDI32, sau đó mới tới tới việc sử dụng Native API của KERNEL32. Trong các phiên bản NT trước 4.0, các hàm “cửa sổ” và “đồ họa” nằm trong Win32 server (CSRSS.EXE). Điều này có nghĩa rằng bất cứ khi nào 1 ứng dụng sử dụng các hàm này, sẽ xuất hiện thông điệp gửi tới server. Từ NT 4.0, các hàm “cửa sổ” và “đồ họa” đã được chuyển vào 1 thành phần kernel-mode là WIN32K.SYS. Thay vì gửi thông điệp tới server, các client-side DLL sẽ gọi trực tiếp tới kernel, tiết kiệm được chi phí gửi thông điệp và chuyển đổi ngữ cảnh tiến trình. Điều này cải thiện đáng kể hiệu suất đồ họa của WinNT (1 bằng chứng là trò chơi Pinball). Như vậy, có thể nói các hàm GDI và USER là Native API thứ hai của WinNT, chỉ khác 1 điều là chúng ít bí hiểm hơn Native API thường do được công bố rộng rãi.
Hình 2 mô tả dòng điều khiển từ ứng dụng Win32 gọi 1 hàm Win32 API (CreateFile()), qua KERNEL32, NTDLL và vào kernel-mode, tại đó điều khiển được chuyển tới dịch vụ hệ thống NtCreateFile. 
 

Hình 2. Cơ chế hoạt động của WinNT Native API

Ntdll.dll cung cấp cho kernel32.dll rất nhiều các hàm tiện ích, cơ bản có thể chia ra làm 2 tập hợp:
• Các hàm runtime được chạy hoàn toàn tại user-mode. Tập hợp này bao gồm cả 1 số phần của C runtime library chuẩn 
• Các kernel function wrapper thực hiện chuyển đổi từ user-mode tới kernel-mode và ngược lại.
Các hàm ở tập thứ 2 cho phép các ứng dụng user-mode gọi các dịch vụ hệ thống tại kernel-mode. Thật thú vị, vậy việc này được thực hiện như thế nào?
Nếu bạn disassemble các hàm bắt đầu bằng Nt hay Zw trong ntdll.dll, VD NtQuerySystemInformation() như ở Mã dẫn 1, bạn sẽ thấy nó rất ngắn gọn. Chỉ đơn giản là 1 lời gọi ngắt? Thực ra cũng không hẳn như vậy. Với những ai đã từng quen thuộc với lập trình DOS, hầu hết các dịch vụ hệ thống của DOS được gọi bằng cách gán mã dịch vụ cho thanh ghi AH, và tham số dữ liệu bổ sung (nếu có) cho thanh ghi DX, theo sau là chỉ thị gọi ngắt INT 21h. Mã thực hiện trong NtQuerySystemInformation() - hay các hàm họ Nt*() - cũng tương tự như vậy: thanh ghi EAX chứa mã dịch vụ, EDX trỏ tới tham số đầu tiên trong stack của hàm.

Mã dẫn 1 Mã thực thi của ntdll.NtQuerySystemInformation()
NtQuerySystemInformation:
  mov eax, 97h
  lea edx, [esp+4]
  int 2Eh
  ret 10h

Chỉ thị đầu tiên nạp cho thanh ghi EAX chỉ số của hàm Native API - mỗi hàm Native API có 1 chỉ số duy nhất. Chỉ thị thứ hai nạp cho thanh ghi EDX con trỏ tới danh sách tham số của hàm. Tiếp theo là 1 chỉ thị gọi ngắt mềm - trên họ x86, ngắt này là 2Eh. Chỉ thị cuối cùng đẩy tất cả tham số ra khỏi stack của hàm gọi. Chúng ta hãy xem xét kĩ chỉ thị gọi ngắt 2Eh.
Nếu như trong DOS, ngắt 21h chỉ đơn giản bắt CPU nhảy tới 1 địa chỉ trong Bảng Vector ngắt (IVT) thì trong HĐH được bảo vệ như WinNT, các ngắt được thực hiện có hơi khác 1 chút. IVT được thay bằng Bảng Bộ mô tả ngắt (IDT) chứa các bộ mô tả bộ nhớ của các địa chỉ đích chứ không đơn thuần là các con trỏ. Trong trường hợp ngắt 2Eh, IDT cung cấp 1 cổng chuyển đổi CPU từ user-mode tới kernel-mode trước khi nhảy tới địa chỉ đích, và chuyển về user-mode khi lời gọi kết thúc. Địa chỉ đích của cổng INT 2Eh nằm trong module ntoskrnl.exe có tên là KiSystemService().
Nhiệm vụ của KiSystemService() là kiểm tra chỉ số của hàm Native API, nếu hợp lệ, nó sẽ chuyển điều khiển tới 1 dịch vụ hệ thống tương ứng trong kernel-mode bằng cách: convert chỉ số hàm Native API thành chỉ số của 1 mảng bên trong kernel - gọi là KiSystemServiceTable. Mỗi chỉ mục trong mảng này lại bao gồm 1 con trỏ tới hàm kernel tương ứng và số tham số của hàm đó (với các hàm Win32 Native API, chỉ số lại trỏ tới 1 mảng thứ 2 của dịch vụ hệ thống. Các con trỏ trong mảng thứ hai này tham chiếu tới các hàm nằm trong WIN32K.SYS). KiSystemService() sau đó sẽ lấy các tham số trong user-mode stack (trỏ tới bởi thanh ghi EDX trong x86) đẩy vào kernel-mode stack, rồi kích hoạt hàm kernel tương ứng (các hàm này thường được cung cấp bởi các hệ thống con NT Executive, nằm hoàn toàn trong kernel-mode, như Process Manager, Virtual Memory Manager hay I/O Manager). Thực ra, cơ chế này có phức tạp hơn 1 chút. Nếu bạn quan tâm thì có thể xem trong Chương 2 cuốn Undocumented Windows 2000 Secrets.
Khi trở về, điều khiển sẽ được trả lại hàm user-mode bằng hàm KiServiceExit() - cũng nằm trong ntoskrnl.exe.
Mỗi dịch vụ hệ thống tất nhiên đều thực hiện 1 hoạt động nhất định phục vụ cho hàm API tương ứng, tuy nhiên, hầu hết trong số chúng đều cần thẩm tra các tham số được truyền tới từ user-mode. Một ít các tham số này là con trỏ, và việc tham chiếu ngược 1 con trỏ bất hợp lệ từ kernel-mode có thể gây ra 1 thảm họa. Giả sử có 1 chương trình nào đó chặn các hàm Native API lại rồi gài vào đó các tham số không thích hợp, 1 số dịch vụ hệ thống sẽ thất bại trong việc thẩm tra các tham số này, kết quả là 1 màn hình xanh xanh sẽ hiện ra... Microsoft đã nhận thức được vấn đề trên và tung ra các miếng vá trong các bản Service Pack.
Một điều cần lưu ‎ là các tham số của họ hàm Nt*() cũng giống như của họ Zw*() – mà một số được công bố rõ trong Windows NT DDK. Theo Bảng chú giải Kernel Mode của NT (NT’s Kernel Mode Glossary), họ hàm Nt*() sẽ thẩm tra các tham số rồi thiết lập tường minh hệ truy nhập trước đó thành USER mode, còn họ hàm Zw*() thì không. Vì thế NT Drivers sẽ gọi các hàm Zw*(), còn các Hệ thống con môi trường HĐH (hay thực sự là các ứng dụng native NT) sẽ gọi các hàm Nt*(), do chúng được gọi từ user-mode.
to be continued...
4. Các hàm NTAPI
Như trên đã nói, các hàm NT API không được Microsoft công bố, vậy làm sao chúng ta có thể biết – và sử dụng được – những hàm này? Tất nhiên là các hàm native API đều nằm trong ntdll.dll, nhưng để sử dụng được chúng ta phải biết được danh sách các definition (constant, type…), các hàm cùng signature – các tham số và giá trị trả về - của chúng. Những cái này thông thường nằm trong file .h do nhà sản xuất cung cấp. Tuy nhiên, do thực sự không có cái gọi là Microsoft ntdll.h, các file ntdll.h tồn tại hiện hay là đều do các hacker/craker bằng cách nào đó khám phá ra, và vì thế rất khác nhau. Dưới đây xin giới thiệu 1 số link để các bạn có thể tham khảo:
- Danh sách 1 số Native API cùng các hàm Win32 API tương ứng của chúng: http://www.sysinternals.com/ntw2k/info/ntdll.shtml
- Danh sách các definition cùng các hàm và signatures tương ứng: http://undocumented.ntinternals.net
- Một file ntdll.h: http://cvs.kldp.net/cgi...l.h?cvsroot=mogua#rev1.5
(Tất cả cũng đã được gói ở đây)
Note: 1 số definition bạn có thể tìm thấy trong các DDK header file, tuy nhiên cần chú Ý khi #include những header files trên vào ứng dụng user-mode thông thường viết bằng C do có thể có xung đột với 1 số header file của Win32 SDK.
5. Xây dựng chương trình sử dụng NT Native API
Có 2 cách để xây dựng ứng dụng sử dụng Native API, tương ứng với 2 kiểu liên kết của thư viện: run-time và link-time 
1. Sử dụng kiểu run-time khi bạn không có, hoặc không muốn sử dụng các file ntdll.lib/ntdll.h
- Định nghĩa các definition và kiểu dữ liệu con trỏ hàm tương ứng với signature của hàm Native API, VD:
//for NtQuerySystemInformation
typedef NTSTATUS (NTAPI *NTQUERYSYSINFO)(SYSTEMINFOCLASS,PVOID,DWORD,PDWORD);
- Trong chương trình, sử dụng GetProcAddress()để lấy con trỏ hàm này từ ntdll, VD:
NTQUERYSYSINFO pNtQuerySysInfo= (NTQUERYSYSINFO)GetProcAddress( GetModuleHandle( _T("ntdll")),"NtQuerySystemInformation");
2. Để sử dụng kiểu link-time, bạn bắt buộc phải có 2 file ntdll.lib và ntdll.h có chứa hàm Native API cần sử dụng (tài liệu có nói file ntdll.lib có trong Platform SDK, nhưng tôi tìm mãi không thấy, tuy nhiên, cũng có cách để bạn tự tạo file này)
- include ntdll.h
- import ntdll.lib vào danh sách các import library (trong VS 6 bạn chọn Project/Settings/Link tab rồi add ntdll.lib vào Object/library modules, trong VS 7: Project/Properties/Linker/Input/Additional Dependenciess). Hoặc nếu không, sử dụng #pragma comment (linker, "/defaultlib:ntdll.lib") trong header file của chương trình. 
Demo project
6. Kết luận
Vậy liệu đây có phải là 1 cách mới để bạn viết các ứng dụng user-mode? Và bạn được gì khi "going native" hay sử dụng trực tiếp các dịch vụ hệ thống của Win NT?
Thực ra, bạn sẽ gặp nhiều rắc rối. Bởi vì NT API không được công bố hay hỗ trợ, nên sẽ không có ai ở Microsoft để bạn phàn nàn nếu như giao diện này thay đổi hay không làm việc như Ý. Thêm nữa, bạn sẽ phải tự tay gánh vác rất nhiều việc, như chỉ ra đường dẫn đầy đủ khi mở file chẳng hạn. Và nhớ rằng, NT API không phải được thiết kế làm 1 giao diện người dùng cuối
Vậy cuối cùng thì có lí do nào để ta sử dụng NT API trực tiếp? Tất nhiên là có. Ít nhất là cũng có 1 thứ mà bạn không thể làm được nếu không sử dụng NT Native API, đó là việc hủy bỏ các yêu cầu I/O (hàm NtCancelIoFile()) – một điều không thể đối với Win32 API. Thêm nữa, tìm hiểu về NT API cũng khiến bạn hiểu rõ hơn về WinNT cũng như cơ chế hoạt động của nó. Và cuối cùng, last but not least: Native API có lẽ giờ đã không còn là điều gì đó bí ẩn đối với bạn nữa 8))
et tổng hợp

7. Tài liệu tham khảo:
[1] Giáo trình Hệ điều hành nâng cao
[2] The Foundations of Microsoft Windows NT System Architecture
[3] Windows 2000 Architecture
[4] Programming WinNT4 Unleashed
[5] Using NT API for file I/O
[6] Inside Native API
[7] Inside Native Applications
[8] Interfacing the the Native API in Windows 2000
[9] Undocumented Windows 2000 Secrets
[10] Inside Windows NT