在Microsoft Windows中,每個進程都有它自己的私有地址空間。當使用&&keyword=%D6%B8%D5%EB&Submit=+%CB%D1%CB%F7+">指針來引用內存時,&&keyword=%D6%B8%D5%EB&Submit=+%CB%D1%CB%F7+">指針的值將引用你自己進程的地址空間中的一個內存地址。你的進程不能創建一銎湟用屬於另一個進程的內存&&keyWord=%D6%B8%D5%EB&Submit=+%CB%D1%CB%F7+">指針。因此,如果你的進程存在一個錯誤,改寫了一個隨機地址系內存,那麼這個錯誤不會影響另一個進程使用的內存。Windows 98 在Windows 98下運行的各個進程共享2 GB的地址空間,該地址空間從0x80000000至0xFFFFFFFF。只有內存映像文件和統組件才能映射到這個區域。
獨立的地址空間對於編程人員和用戶來說都是非常有利的。對於編程人員來說,系統更容易捕獲隨意的內存讀取和寫入操作。對於用戶來說,/Article/czxt/">操作系統將變得更加健壯,因個應用程序無法破壞另一個進程或/Article/czxt/">操作系統的運行。當然,/Article/czxt/Index.Html">操作系統的這個健壯特性是要凍代價的,因為要編寫能夠與其他進程進行通信,或者能夠對其他進程進行操作的應用程序難得多。
有些情況下,必須打破進程的界限,訪問另一個進程的地址空間,這些情況包括:
當你想要為另一個進程創建的窗口建立子類時。
當你需要調試幫助時(例如,當你需要確定另一個進程正在使用哪個DLL時)。
當你想要掛接其他進程時。
這裡將介紹兩種方法,可以用來將DLL插入到另一個進程的地址空間中。一旦你的DLL進入另一個進程的地址空間,就可以對另一個進程為所欲為。這一定會使你非常害怕,因此,究竟應該怎樣做,要三思而後行。
1 插入DLL:一個例子
假設你想為由另一個進程創建的窗口建立一個子類。你可能記得,建立子類就能夠改變窗口的行為特性。若要建立子類,只需要調用SetWindowLongPtr&&函數,改變窗口的內婵中的窗口過程地址,指向一個新的(你自己的) WndProc。Platform SDK文檔說,應用程虿能為另一個進程創建的窗口建立子類。這並不完全正確。為另一個進程的窗口建立子類的關鍵問題與進程地址空間的邊界有關。
當調用下面所示的SetWindowsLongPtr&&函數,建立一個窗口的子類時,你告訴系統,發送到或者顯示在hwnd設定的窗口中的所有消息都應該送往MySubclassProc,而不是送往口的正常窗口過程:
進程A中代碼:
EXE file:
LRESUlT WndProc(HWND hend,UNIT uMsg,...){.....}
USER32.DLL file
LONG DispatchMessage(CONST MSG*msg)
{
LONG lRet;
WNDPROC lpfnWndProc=
(WNDPROC)GetWindowLongPtr(msg,hwnd,GWLP_WNDPROC
);
lRet=lpfnWndProc(msg.hwnd,msg.message,msg.wParam,mag.
lParam);
return lRet;
}
進程B中:
EXE file
void Somefunc(void)
{
HWND hwnd=Findwindow("class A",NULL);
SetWindowLongPtr(hwnd,GWLP_WNDPROC,MySubclassProc);
}
USER32.DLL file ......
換句話說,當系統需要將消息發送到指定窗口的WndProc時,要查看它的地址,然後直接調用WndProc。在本例中,系統發現MySubclassProc&&函數的地址與窗口相關聯,因此就直接調用MySubclassProc&&函數。
為另一個進程創建的窗口建立子類時遇到的問題是,建立子類的過程位於另一個地址空間中。下面舉個例子,說明窗口過程是如何接受消息的。進程A正在運行,並且已經創建了一個窗口。文件User32.dll被映射到進程A的地址空間中。
對User32.dll文件的映射是為了接收和發送在進程A中運行的任何線程創建的任何窗口中發送和顯示的消息。當User32.dll的映像發現一個消息時,它首先要確定窗口的WndProc的地址,然後調用該地址,傳遞窗口的句柄、消息和wParam和lParam值。當WndProc處理該消息後,User32.dll便循環運行,並等待另一個窗口消息被處理。
進程B中的線程試圖為進程A中的線程創建的窗口建立子類現在假設你的進程是進程B,你想為進程A中的線程創建的窗口建立子類。你在進程B中的代碼必須首先確定你想要建立子類的窗口的句柄。這個操作使用的方法很多。上面的例子只是調用FindWindow&&函數來獲得需要的窗口。接著,進程B中的線程調用SetWindowLongPtr&&函數,試圖改變窗口的WndProc的地址。請注意我說的“試圖”二字。這個&&函數調用⒉進行什麼操作,它只是返回NULL。SetWindowLongPtr&&函數中的代碼要查看是否有一個進程正在試圖改變另一個進程創建的窗口的WndProc地址,然後將忽略這個&&函數的調用。
如果SetWindowLongPtr&&函數能夠改變窗口的WndProc,那將出現什麼情況呢?系統將把MySubclassProc的地址與特定的窗口關聯起來。然後,當有一條消息被發送到這個窗口中時,進程A中的User32代碼將檢索該消息,獲得MySubclassProc的地址,並試圖調用這個地址。但是,這時可能遇到一個大問題。MySubclassProc將位於進程B的地址空間中,而進程A是個活動進程。顯然,如果User32想要調用該地址,它就要調用進程A的地址空間中的一個地址,這就可能造成內存訪問的違規。
為了避免這個問題的產生,應該讓系統知道M y S u b c l a s s P r o c是在進程B的地址空間中,然後,在調用子類的過程之前,讓系統執行一次上下文轉換。M i c r o s o f t沒有實現這個輔助&&函數功能,原因是:應用程序很少需要為其他進程的線程創建的窗口建立子類。大多數應用程序只是為它們自己創建的窗口建立子類,Wi n d o w s的內存結構並不阻止這種創建操作。
切換活動進程需要占用許多C P U時間。
進程B中的線程必須執行M y S u b c l a s s P r o c中的代碼。系統究竟應該使用哪個線程呢?是現有的線程,還是新線程呢?
U s e r 3 2 . d l l怎樣才能說明與窗口相關的地址是用於另一個進程中的過程,還是用於同一個進程中的過程呢?
由於對這個問題的解決並沒有什麼萬全之策,因此M i c r o s o f t決定不讓S e t Wi n d o w s L o n g P t r改變另一個進程創建的窗口過程。不過仍然可以為另一個進程創建的窗口建立子類—只需要用另一種方法來進行這項操作。這並不是建立子類的問題,而是進程的地址空間邊界的問題。如果能將你的子類過程的代碼放入進程A的地址空間,就可以方便地調用S e t Wi n d o w L o n g P t r&&函數,將進程A的地址傳遞給M y S u b c l a s s P r o c&&函數。我將這個方法稱為將D L L“插入”進程的地址空間。有若干種方法可以用來進行這項操作。下面將逐個介紹它們
2.通過掛鉤插入DLL
可以使用掛鉤將D L L插入進程的地址空間。為了使掛鉤能夠像它們在1 6位Wi n d o w s中那樣工作,M i c r o s o f t不得不設計了一種方法,使得D L L能夠插入另一個進程的地址空間中。
下面讓我們來看一個例子。進程A(類似Microsoft Spy++的一個實用程序)安裝了一個掛鉤W N _ G E T M E S S A G E,以便查看系統中的各個窗口處理的消息。該掛鉤是通過調用下面的S e t Wi n d o w s H o o k E x&&函數來安裝的:
第一個參數W H _ G E T M E S S A G E用於指明要安裝的掛鉤的類型。第二個參數G e t M s g P r o c用於指明窗口准備處理一個消息時系統應該調用的&&函數的地址(在你的刂房占渲校5三個參數h i n s t D l l用於指明包含G e t M s g P r o c&&函數的D L L。在Wi n d o w s中,D L L的h i n s t D l l的值用於標識DLL被映射到的進程的地址空間中的虛擬內存地址。最後一個參數0用於指明要掛接的線程。
對於一個線程來說,它可以調用S e t Wi n d o w s H o o k E x&&函數,傳遞系統中的另一個線程的I D。通過為這個參數傳遞0,就告訴系統說,我們想要掛接系統中的所有G U I線程。
現在讓我們來看一看將會發生什麼情況:
1) 進程B中的一個線程准備將一條消息發送到一個窗口。
2) 系統查看該線程上是否已經安裝了W H _ G E T M E S S A G E掛鉤。
3) 系統查看包含G e t M s g P r o c&&函數的D L L是否被映射到進程B的地址空間中。
4) 如果該D L L尚未被映射,系統將強制該D L L映射到進程B的地址空間,並且將進程B中的D L L映像的自動跟蹤計數遞增1。
5) 當D L L的h i n s t D l l用於進程B時,系統查看該&&函數,並檢查該D L L的h i n s t D l l是否與它用於進程A時所處的位置相同。
如果兩個h i n s t D l l是在相同的位置上,那麼G e t M s g P r o c&&函數的內存地址在兩個進程的地址空間中的位置也是相同的。在這種情況下,系統只需要調用進程A的地址空間中的G e t M s g P r o c&&函數即可。
如果h i n s t D l l的位置不同,那麼系統必須確定進程B的地址空間中G e t M s g P r o c&&函數的虛擬內存地址。這個地址可以使用下面的公式來確定:
將GetMsgProc A的地址減去hinstDll A的地址,就可以得到G e t M s g P r o c&&函數的地址位移(以字節為計量單位)。將這個位移與hinstDll B的地址相加,就得出G e t M s g P r o c&&函數在用於進程B的地址空間中該D L L的映像時它的位置。
6) 系統將進程B中的D L L映像的自動跟蹤計數遞增1。
7) 系統調用進程B的地址空間中的G e t M s g P r o c&&函數。
8) 當G e t M s g P r o c&&函數返回時,系統將進程B中的D L L映像的自動跟蹤計數遞減1。
注意,當系統插入或者映射包含掛鉤過濾器&&函數的D L L時,整個D L L均被映射,而只是掛鉤過濾器&&函數被映射。這意味著D L L中包含的任何一個&&函數或所有&&函數現在都存在,並且可以從進程B的環境下運行的線程中調用。
若要為另一個進程中的線程創建的窗口建立子類,首先可以在創建該窗口的掛鉤上設置一個W H _ G E T M E S S A G E掛鉤,然後,當G e t M s g P r o c&&函數被調用時,調用S e t Wi n d o w L o n g P t r&&函數 來建立窗口的子類。當然,子類的過程必須與G e t M s g P r o c&&函數位於同一個D LL中。
與插入D L L的注冊表方法不同,這個方法允許你在另一個進程的地址空間中不再需要DL L時刪除該D L L的映像,方法是調用下面的&&函數:
當一個線程調用U n h o o k Wi n d o w s H o o k E x&&函數時,系統將遍歷它必須將D L L插入到的各個進程的內部列表,並且對D L L的自動跟蹤計數進行遞減。當自動跟蹤計數時,D L L就自動從進程的地址空間中被刪除。應該記得,就在系統調用G e t M s g P r o c前,它對D L L的自動跟蹤計數進行了遞增(見上面的第6個步驟)。
該自動跟蹤計數沒有遞增,那麼當進程B的線程試圖執行G e t M s g P r o c&&函數中的代胧,系統中運行的另一個線程就可以調用U n l o o k Wi n d o w s H o o k E x&&函數。
這一切意味著不能撤消該窗口的子類並且立即撤消該掛鉤。該掛鉤必須在該子類的壽命期內保持有效狀態。
3.使用遠程線程插入DLL
插入D L L的另一個方法是使用遠程線程。這種方法具有更大的靈活性。它要求你懂得若干個Wi n d o w s特性、如進程、線程、線程同步、虛擬內存管理、D L L和U n i c o d e等(如果對這些特性不清楚,請參閱本書中的有關章節)。Wi n d o w s的大多數&&函數市斫討歡宰己進行操作。
這是很好的一個特性,因為它能夠防止一個進程破壞另一個進程的運行。但是,有些&&函數卻允許一個進程對另一個進程進行操作。這些&&函數大部分最初是為調試程序和其他工具杓的。不過任何&&函數都可以調用這些&&函數。
這個D L L插入方法基本上要求目標進程中的線程調用L o a d L i b r a r y&&函數來載必要的D L L。
由於除了自己進程中的線程外,我們無法方便地控制其他進程中的線程,因此這種解決方案要求我們在目標進程中創建一個新線程。由於是自己創建這個線程,因此我們能夠控撲執行什麼代碼。幸好,Wi n d o w s提供了一個稱為C r e a t e R e m o t e T h r e a d的&&函數,使我們能夠非常容易地在另一個進程中創建線程:
C r e a t e R e m o t e T h r e a d與C r e a t e T h r e a d很相似,差別在於它增加了一個參數h P r o c e s s。該參數指明擁有新創建線程的進程。參數p f n S t a r t A d d r指明線程&&函數的內存地址。當然,該內存地址與遠程進程是相關的。線程&&函數的代碼不能位於你自己進程的地址空間中。
Windows 98 在Windows 98中,C r e a t e R e m o t e T h r e a d&&函數不存在有用的實現代碼,它只是返回N U L L。調用G e t L a s t E r r o r&&函數將返回E R R O R _ C A L L _ N O T _ I M P L E M E N T E D(C r e a t e T h r e a d&&函數包含用於在調用進程中創建線程的完整的實現代碼)。由於C r e a t e R e m o t e T h r e a d沒有實現,因此,在Windows 98下,不能使用本方法來插入D L L。
好了,現在你已經知道如何在另一個進程中創建線程了,但是,如何才能讓該線程加載我們的D L L呢?答案很簡單,那就是需要該線程調用L o a d L i b r a r y&&函數。
我們將詳細步驟說明如下:
1) 使用Vi r t u a l A l l o c E x&&函數,分配遠程進程的地址空間中的內存。
2) 使用Wr i t e P r o c e s s M e m o r y&&函數,將D L L的路徑名拷貝到第一個步驟中已經分配的內存中。
3) 使用G e t P r o c A d d r e s s&&函數,獲取L o a d L i b r a r y A或L o a dL i b r a t y W&&函數的實地址(在K e r n e l 3 2 . d l l中)。
4) 使用C r e a t e R e m o t e T h r e a d&&函數,在遠程進程中創建一個線程,它調用正確的L o a d L i b r a r y&&函數,為它傳遞第一個步驟中分配的內存的地址。這時, D L L已經被插入遠程進程的地址空間中,同時D L L的D l l M a i n&&函數接收到一個D L L _ P R O C E S S _ AT TA C H通知,並且能夠執行需要的代碼。當D l l M a in&&函數返回時,遠程線程
從它對L o a d L i b r a r y的調用返回到B a s e T h r e a d S t a r t &&函數(第6 章中已經介紹。然後B a s e T h r e a d S t a r t調用E x i t T h r e a d,使遠程線程終止運行。
現在遠程進程擁有第一個街柚蟹峙涞哪诖婵椋鳧 L L則仍然保留在它的地址空間中。
若要將它刪除,需要在遠程線程退出後執行下面的步驟:
5) 使用Vi r t u a l F r e e E x&&函數,釋放第一個步驟中分配的內存。
6) 使用G e t P r o c A d d r e s s&&函數,獲得F r e e L i b r a r y&&函數的實地址(在K e r n e l 3 2 . d l l中)。
7) 使用C r e a t e R e m o t e T h r e a d&&函數,在遠程進程中創建一個線程,它調用F r e e L i b r a r y&&函數,傳遞遠程D L L的H I N S TA N C E。
這就是它的基本操作步驟。這種插入D L L的方法存在的唯一一個不足是, Windows 98並不支持這樣的&&函數。只能在Windows 2000上使用這種方法。