再談Windows NT/2000內部資料結構
WebCrazy(tsu00@263.net)
注:本文最初見於www.nsfocus.com
現在我們結合Regmon(http://www.sysinternals.com/)在NT中的實現方法再來談談Windows NT/2000內部資料結構。
Regmon是監視應用程式訪問系統註冊表的公用程式。大家都知道在應用程式中使用註冊表一般都調用WinAPI Regxxx,而Regxxx最終會調用Native API Zwxxx!(參閱Windows NT/2000 DDK Documentation)。Regmon正是通過改變這些常式以達到監視註冊表的目的。Zwxxx的實現方式如下:
mov eax, ServiceId
lea edx, ParameterTable
int 2eh
ret ParamTableBytes
這就是所說的NT System Services,是不是與Linux有點相似(只不過Linux使用的是80h中斷而已,它也有ServiceID,如fork系統調用ServiceID為2)。
System Services在DDK Documentation是如下定義的:
The set of native, user-mode routines exported by the executive for use only by protected subsystems. Each system service has a name of the form TwoLettersXxxYyy where:
TwoLetters is the prefix for all system services.
Xxx is usually a verb, describing the operation of a given service.
Yyy is generally the object type the service operates on.
System Services在系統中由兩部分組成,一部分由win32k.sys匯出,另一部分由ntoskrnl.exe提供服務。前者主要完成NT中win32、Posix與Os/2等子系統(subsystems)與核心的通訊,僅能由使用者態的應用程式調用,如user32!WaitMessage等。由於Regmon只涉及後者,所以本文將對其進行討論,以下所有關於System Service的討論均適合兩者!
上次(Nsfocus Magazine 10)我曾經提及KeServiceDescriptorTable,也說過它的結構如下:
struct _ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
}ServiceDescriptorTableEntry
ntoskrnl.exe匯出全域變數KeServiceDescriptorTable指向ServiceDescriptorTableEntry(由win32k.sys匯出的System Services也有自己的ServiceDescriptorTable,在Win2000 Server中其Service ID從1000h始,由KeServiceDescriptorTable以下位移50h處指向,其結構與ntosrknl.exe匯出的基本一致,本文不作討論,SoftICE中的ntcall命令在特定情況下可以列出所有的System Service)。
下面我們先用SoftICE 4.05 For Windows NT/2000來分析分析x86平台Windows 2000 Server Build 2195的情況(以下僅摘錄部分,不同版本不同時刻可能得到的資料未必一樣)
:dd KeServiceDescriptorTable l 4*4
//如果看win32k.sys匯出的表請使用dd KeServiceDescriptorTable+50 l 4*4
//即將KeServiceDescriptorTable向下位移50h處下同
0008:8046AB80 804704D8 00000000 000000F8 804708BC ..G...........G.
| |_ServiceTableBase值 | | |_ParamTableBase值
| |_似乎總為0 |
|_KeServiceDescriptorTable地址 |_NumberOfService
:dd @KeServiceDescriptorTable l byte(@(KeServiceDescriptorTable+08))*4
// dd ServiceDescriptorTableEntry->ServiceTableBase l NumberOfService*4
0008:804704D8 804AB3BF 804AE86B 804BDEF3 8050B034 ..J.k.J...K.4.P.
|
|_ServiceID=0的System Service入口地址(依次類推)
0008:804704E8 804C11F4 80459214 8050C2FF 8050C33F ..L...E...P.?.P.
0008:804704F8 804B581C 80508874 8049860A 804FC7E2 .XK.t.P...I...O.
0008:80470508 804955F7 8049C8A6 80448472 804A8D50 .UI...I.r.D.P.J.
0008:80470518 804B6BFB 804F0CEF 804FCB95 8040189A .kK...O...O...@.
0008:80470528 804D06CB 80418F66 804F69D4 8049E0CC ..M.f.A..iO...I.
...(略)
:db @(KeServiceDescriptorTable+0c) l byte(@(KeServiceDescriptorTable+08))
// db ServiceDescriptorTableEntry->ParamTableBase
0008:804708BC 18 20 2C 2C 40 2C 40 44-0C 18 18 08 04 04 0C 10 . ,,@,@D........
|
|_ServiceID=0的System Service參數個數*4(即參數個數為18h/4=6)
0008:804708CC 18 08 08 0C 08 08 04 04-04 0C 04 20 08 0C 14 0C ........... ....
...(略)
要獲得哪個應用程式對系統註冊表有過操作,只要在對其有操作的System Service中注入自己的代碼,也就是改變這些System Service的執行流程,先執行自己的代碼(Regmon中用於記錄供GUI部分使用),接著返回至原先處繼續執行即可。通過以上分析,我們知道只要修改ServiceTableBase到ServiceTableBase+NumberOfService*4範圍的資料就可以改變System Service的執行流程,而只要知道System Service的ServiceID就可以改變這一System Service入口地址在這一地區的位置,那麼又如何得到System Service的Service ID呢!我們可以隨便以ZwOpenKey作個例子:
:u ZwOpenKey
ntoskrnl!ZwOpenKey
0008:80400E2A B867000000 MOV EAX,00000067
| |_ServiceID
|_機器碼(其中第二位元組即ZwOpenKey線性地址加一處就是ServiceID)
0008:80400E2F 8D542404 LEA EDX,[ESP+04]
0008:80400E33 CD2E INT 2E
0008:80400E35 C20C00 RET 000C
這樣只要知道Zwxxx常式名(即System Service在記憶體中的線性地址),是不是就可以實現我們的目的了呢?來看看Regmon的具體實現代碼吧:
.
.
.
// 儲存ZwOpenKey原先入口,在HookRegOpenKey中使用
RealRegOpenKey = SYSCALL( ZwOpenKey );
// 修改ZwOpenKey流程,指向新的入口,即調用ZwOpenKey時轉向執行HookRegOpenKey
SYSCALL( ZwOpenKey ) = (PVOID) HookRegOpenKey;
.
.
.
SYSCALL在intel平台是如下定義的:
#define SYSCALL(_function) ServiceTable->ServiceTable[ *(PULONG)((PUCHAR)_function+1)]
ServiceTable->ServiceTable就是我們上面所述的 ServiceDescriptorTableEntry->ServiceTableBase(為了便於描述)。_function+1即ServiceID所在地址。整個運算式即取得_function對應的System Service的入口地址線上性記憶體中的位置。其它定義請參閱Regsys.c與Regsys.h!
可以使用SoftICE對比一下Regsys.sys裝載前後ServiceTable中System Service入口地址的變化,加深對System Service攔截的理解.
好了現在我們知道Regmon的基本實現方法了(當然真正要實現此功能還要考慮很多問題,如保護態應用程式與核心驅動程式之間的通訊、線程同步等等)。
讓我們再來看看KeServiceDescriptorTable的另一個應用吧!如果我們重新分配段記憶體池,構造自己的ServiceTable與ParamTable數組(必須複製系統原有的System Services,否則...),然後修改結構中ServiceTableBase與ParamTableBase,使其指向自己的ServiceTable與ParamTable,再修改一下NumberOfServices,是不是可以增加自個兒的System Service呢!如果你有興趣的話可以參閱<<UNDOCUMENTED NT>>。這書我也沒見過,只知道網上它名聲在外。哦,還要感謝James Shatlyk給我提供隨書配套例子代碼。如果您見過此書(不知道有沒有Chinese 版,E文也可),能不能與我聯絡聯絡?
談完System Service後,再讓我們來看看Regmon是如何在Driver中取得系統進程名的。
首先談談KTEB(Kernel Thread Environment Block)與KPEB(Kernel Process Environment Block),與TEB(其實應該是User-TEB)一樣,KPEB/KTEB則紀錄著系統核心進程/線程資訊。要瞭解KTEB、KPEB,首先要知道如何得到當前進程/線程中它們的基址,可以先看看Native API IoGetCurrentProcess。在Windows 2000 DDK Document中它是如下定義的:
PEPROCESS IoGetCurrentProcess();
使用IDA Pro或SoftICE,可知其在ntoskrnl.exe僅是由幾條彙編指令實現的:
mov eax,fs:[00000124]
mov eax,[eax+00000044] //NT 4.0以下這個值應為[eax+40]
ret
這個Native API很有典型性,它的第一條指令取得當前線程的KTEB,而整個API剛好將相對於KTEB始68(即16進位44)位元組處取得當前進程的KPEB返回給使用者。你可以使用SoftICE驗證一下。
讓我們再來看看其具體是如何?的:
//----------------------------------------------------------------------
//
// GetProcessNameOffset
//
// In an effort to remain version-independent, rather than using a
// hard-coded into the KPEB (Kernel Process Environment Block), we
// scan the KPEB looking for the name, which should match that
// of the GUI process
//
//----------------------------------------------------------------------
ULONG GetProcessNameOffset()
{
PEPROCESS curproc;
int i;
DbgPrint(("GetProcessNameOffset/n"));
curproc = PsGetCurrentProcess();
//
// Scan for 12KB, hopping the KPEB never grows that big!
//
for( i = 0; i < 3*PAGE_SIZE; i++ ) {
if( !strncmp( SYSNAME, (PCHAR) curproc + i, strlen(SYSNAME) )) {
return i;
}
}
//
// Name not found - oh, well
//
return 0;
}
//----------------------------------------------------------------------
//
// GetProcess
//
// Uses undocumented data structure offsets to obtain the name of the
// currently executing process.
//
//----------------------------------------------------------------------
FILTERSTATUS GetProcess( PCHAR Name )
{
PEPROCESS curproc;
char *nameptr;
ULONG i;
//
// We only try and get the name if we located the name offset
//
if( ProcessNameOffset ) {
curproc = PsGetCurrentProcess();
nameptr = (PCHAR) curproc + ProcessNameOffset;
strncpy( Name, nameptr, 16 );
} else {
strcpy( Name, "???");
}
.
.
.
}
這段代碼從Regmon中NT Driver部分摘錄,詳細可參閱Regsys.c。
這兩函數主要功能是取得進程名稱,供程式使用。大家都知道在Driver部分不能簡單的調用WIN32 API,而NT執行體提供的NtQuerySystemInformation主要針對所有進程、線程或其他NT內部資訊等,所以我們必須尋找其它方法(一般方法是跟蹤相應的Win32 API用Debugger對其進行艱苦但充滿挑戰充滿樂趣的逆向工程,然後找出其在NT執行體中的具體實現過程,你也可以使用此方法對本文所提及的進行驗證)。
Regmon中這兩個函數通過尋找KPEB取得進程名,GetProcessNameOffset主要是調用PsGetCurrentProcess取得KPEB基址,然後搜尋KPEB,得到ProcessName相對KPEB的位移量,存放在全域變數ProcessNameOffset中。在NT/2000 DDK中如下定義PsGetCurrentProcess:
#define PsGetCurrentProcess() IoGetCurrentProcess()
而IoGetCurrentProcess已經在前面討論過了。
作者在3頁記憶體地區(x86中一頁為4k)尋找,從程式中注釋可知他也不知道是否會超出此範圍,還有程式段中SYSNAME被定義為system,因為調用Driver中DriverEntry入口正是由system進程調度(GetProcessNameOffset在DriverEntry中調用)。你也可以使用SoftICE查出特定Windows NT/2000版本中ProcessNameOffset的值。在x86平台Windows 2000 Server Build 2195中它為1fch(NT 4.0與3.51中為1dch),然後根據這個值找幾個進程核對核對。
GetProcess將當前進程的KPEB基址加上ProcessNameOffset值取得當前進程(Regmon中即叫用作業Registry的Native API進程)的名稱。
至於KPEB/KTEB等的具體結構,各位元組的具體含義,由於其所謂的Undocument,我查MSDN,到各新聞群組,追蹤NT核心,也沒找到其中的一小部分,這也是我著手寫此篇的用意,希望懂得的高手,朋友能互相交流交流,還有本文有誤之處,還望您能指出並與我說說,謝謝!
參考資料:
1.Regmon 4.22原始碼
2.Windows 2000 DDK Documentation