Windows 98動態連結函式庫攔截技巧

Windows 98動態連結函式庫攔截技巧


Windows 98動態連結函式庫攔截技巧
▓攔截 WinSock ?
前不久,實驗室的學長遇到一個問題,他們希望可以攔截系統中有關WinSock send()  recv() 的呼叫,也就是說在應用程式呼叫WSOCK32.DLL的函式以前,先把控制權交到我們手裡,等我們處理完以後,再讓原來呼叫的動作可以繼續執行,在找了一些相關書籍後,似乎並沒有一個可以合乎他們要求的解決方法,於是為了解決這個問題,我參考了一些目前我所可以取得的資料
(1) Run PC 39 Win32 Spy 和 DBWIN,侯俊傑
(2) Windows 95 System Programming Secrects ,Matt Pietrek
(3) Advanced Windows,Jeffrey Richter
首先,我們目的是要讓系統中所有用到WSOCK32.DLL send()  recv() 的函式可以透過我們加工之後再讓它去作它該做的事,對於用到WinSock的應用程式像FTP,WWW瀏覽器的應用軟體可以在不必修改原始碼的情況下正常運作
所遇到的第一個問題,便是Win32系統中各個Process分離位址空間的設計,使我們無法讓另一個Process可以Jmp到我們這個行程中來執行,如圖(),Windows 9x2GB以下的空間為每個行程所私有的,如果我們今天同時使用A程式與B程式都用到了WSOCK32.DLL的函式,對程式AB虛擬記憶體空間來說,它們都認為它們的空間中存在著它們自己的WSOCK32.DLL,如圖()所示,WSOCK32.DLLLoad進記憶體之後,實際上是存在2GB的空間以下的,如果程式A試著透過WriteProcessMemory去修改WSOCK32.DLL,實際上會更動到的只有程式A所在空間中的WSOCK32.DLL,對程式B來說是沒有影響的,以我的電腦來說,當我啟動兩個使用WSOCK32.DLL的程式後,WSOCK32.DLL都會位於0x785C0000 的位址,事實上此時兩個應用程式是在2GB位址空間以下透過Windows記憶體管理機制來共享這個DLL所存在的記憶體,當程式ADLL寫入時,則會透過Copy-On-Write,為程式A複製一個DLL的空間,所以就算我們
在程式A中修改了WSOCK32.DLL對程式B來說卻一點影響都沒有,我也曾想過透過修改WSOCK32.DLL檔頭的Image Base的值,使它可以放到記憶體的2GB以上,由各行程來共享同一個DLL,不過後來試驗證明這並不是一個可行的好方法

()
■ DLL放入2 GB以上
在經過一番思考後,我決定寫一個DLL放到2GB的空間,由每個行程來共享這個DLL,可是我該如何得知系統目前有那些的Process有用到WSOCK32.DLL?在找尋了MSDN,及一些參考書籍後,我決定透過TOOLHELP函式(KERNEL32.DLLexport出來的,MSDN中有許多可參考的資訊)來完成這項工作,以下列出部份函式
HANDLE WINAPI CreateToolhelp32Snapshot(DWORD dwFlags,DWORD h32ProcessID);
BOOL WINAPI Process32First(HANDLE hSnapshot,LPPROCESSENTRY32 lppe);
BOOL WINAPI Process32Next(HANDLE hSnapshot,LPPROCESSENTRY32 lppe);
BOOL WINAPI Module32First(HANDLE hSnapshot,LPMODULEENTRY32 lpme);
BOOL WINAPI Module32Next(HANDLE hSnapshot,LPMODULEENTRY32 lpme);
但萬事俱備後,我開始去思考如何去對系統中的行程做一個手術,我所想到的第一個方法便是去修改每個行程的Import Table,如下圖(),我們透過SPYSOCK.EXESPYDLL.DLL載入到記憶體的2GB以上的空間(置入2GB以上空間的方法,可以參照Windows 95 System Programming Secrects Chap6中的範例程式)
SPYSOCK.EXE接著透過TOOLHELP函式取的每個Module的基底位址,有了基底位址,我們可以藉此取得.idata區的RVA位址,進而找出是否有在程式中使用到WSOCK32.DLL,若有我們便修改Import Table 中指向send()  recv()函式的位址,把它們指向我們放在2GB以上位址空間的SPYDLL.DLL對應的處理函式,當處理函式把我們所要加工的動作處理完後,SPYDLL.DLL再透過GetModuleHandle(“wsock32.dll”) 得到 ECHOAC32.EXE WSOCK32.DLL所在的基底位址,此時要注意的是,由於分離位址空間的關係此時所得到的位址是ECHOAC32.EXE中的WSOCK32.DLL的位址,並非把SPYDLL.DLL載入到記憶體的SPYSOCK.EXE中的WSOCK32.DLL的位址(當然,此例中SPYSOCK.EXE並未使用WSOCK32.DLL中的任何函式),有了ECHOAC32.EXE的基底位址後,我們便可以算出send() recv()在該行程中的位址,透過如下的動作
_send=(HANDLE (WINAPI *) (SOCKET s, const char FAR * buf, int len, int flags))GetProcAddress(ori_wsock32,”send”);
_send(s,buf,len,flags);
我們便可把原本send()函式的動作給完成了,最後再把處理函式給結束後即可返回原呼叫函式,如此則完成了一個攔截的動作
▓圖(二)
▓完成了嗎?
當讀者們測試過以上這範例後會發現,為何對像NETTERM這類的軟體發揮不了作用呢?因為這類的軟體是採用動態Load WSOCK32.DLL,再執行該DLL的函式,所以我們在它們的Import Table中無法發現WSOCK32.DLL的蹤跡,因此補救的方法還有一種,就是既然要動態載入WSOCK32.DLL,則它必定會透過LoadLibrary(“WSOCK32.DLL”) & GetProcAddress(),所以我們可以在Import Table中把這兩個Function也一併攔下,在對應的處理函式中我們再來檢查是否有用到WSOCK32.DLL的函式,如果有我們也可以透過攔GetProcAddress()來檢查send()recv()所傳回的記憶體位址為何,把該變數位址儲存起來,並指向我們的處理函式,如此亦可做為一個補救的方法,不過,我將在下一節介紹另一種手法,也可以符合我們的目的,供各位做一個寫作上的參考
▓除此之外
我並沒有採用上述的第二種方法來補救無法對動態使用DLL攔截的問題,我所採用的方法為透過修改DLL函式前幾個Byte來跳到我的處理函式,但此種方法有一個弊病,就是如果該函式小於我們所要置入Code的大小,那樣就會發生不可預期的後果了,所幸,在我們所要攔的send() 及 recv()來說,都大於我所要置入的7 bytes,所以這部份還算是可以安然渡過
如下圖三,為我們呼叫 Func(int A,int B,int C) 一個具有三個引數的函式時
堆疊內的情況,而此函式對呼叫者來說會產生如下的Assembly Code
Push C
Push B
Push A
Call Func’s Address

我們可以透過 [EBP+0x10],[EBP+0x0C],[EBP+0x08]在堆疊中取得C,B,A三個引數的值來加以處理
▓圖(三)
如下圖四,假如Send函式共有A,B,C三個引數,我們依舊是透過TOOLHELP的函式來幫助我們找到在那個行程中有使用到wsock32.dll,如果找到了,則依以下的步驟來加以處理
<Step 0> SPYSOCK.EXE透過WriteProcessMemory7 BytesCode如下
MOV EAX,spy_Send//spy_Send指向我們置於2GB以上記憶體處理函式的位址
Call EAX
填入Send 函式的前端
<Step 1> 則是當Process執行到函式Send ,便會跳到我們對應的處理函式<Step 2> 在處理函式執行完對應的處理動作後,把函式Send7 Bytes還原為原本未經修改過的樣子,在返回Send函式後以便繼續把它原本的工作給完成,並修改Send函式的堆疊,以使Send函式在結束處理的程序後,Return回原呼叫的應用程式前,可以先跳到我們repair_send函式來,再把Send函式的前7Bytes給換成
MOV EAX,spy_Send//spy_Send指向我們置於2GB以上記憶體處理函式的位址
Call EAX
如此,在下一次又執行到Send函式時,依舊可以如此的先透過我們來處理
<Step 3>當函式Send執行完原本要做的工作後,Return ,會先跳到repair_send 所在的位址,repair_send負責把函式Send的前7 Bytes再修改成 Step 0 的樣子,使下一次執行到函式Send,可以再透過我們所設置的對應處理函式
<Step 5> 在執行完修補的動作後,便由repair_send函式負責返回函式Send所要Return回去的原呼叫應用程式

(四)
如下圖五
<Step 0> SPYSOCK.EXE在發現NETTERM.EXE所在Process中有使用到 WSOCK32.DLL,透過WriteProcessMemory去修改函式Send的前7 Bytes
<Step 1> NETTERM.EXE使用WSOCK32.DLL的函式Send
<Step 2> 會先執行我們在SPYDLL.DLL中的處理函式
<Step 3> 執行完我們所要處理的動作後,便跳回原函式Send(此時前Send7 Bytes已被置換回原來的樣子,並修改Send的返回位址)
<Step 4> 函式Send在執行結束後,會跳到repair_send,來把函式Send7 Bytes換成Step 0中的樣子,使下一次再呼叫函式Send時可以依此流程再度發生作用
<Step 5> 動作結束後,repair_send函式,返回Send的呼叫端應用程式
本程式原始碼我放在http://www.sparc14.cc.ncku.edu.tw/e9484110(暫定的…^_^)

(五)
Windows 98WSOCK32.DLL
如下圖六,
<Step 1>得知LoadLibraryA的引數為ws2_32.dll,藉此把ws2_32.dll載入到該行程之中
<Step 2>得知LoadLibraryA的引數為mswsock.dll藉此把mswsock.dll載入到該行程之中
<Step 3>得知wsock32.dllgethostbyname函式會透過GetProcAddress取得ws2_32.dll中對應的函式來動作
<Step 4>得知wsock32.dllEnumProtocolsA函式會透過GetProcAddress取得mswsock.dll中對應的函式來動作

▓圖(六)
785C132C該行之反組譯為 MOV [785C6180],EAX ,即把LoadLibraryA(“ws2_32.dll”)傳回的值存在位於記憶體785C6180的位置,同理在785C1335該行,我們可以看到LoadLibraryA(“mswsock.dll”)傳回值存在記憶體785C6184的位置,接著我們可以看到
785C1810 PUSH 785C63C0
785C1815 PUSH DWORD PTR [785C6180]
785C181B CALL DWORD PTR GetProcAddress的位址
其中785C63C0gethostbyname該字串的位址,785C6180位址則存放著LoadLibraryA(“ws2_32.dll”)傳回值,如果你在Help中查詢GetProcAddress函式的用法,可以得到如下資料
GetProcAddress(
HMODULE hModule, // handle to DLL module
LPCSTR lpProcName // name of function );
因此,我們可以藉此得知當我們在wsock32.dll中使用gethostbyname的函式時,它會把工作再轉給ws2_32.dll中的gethostbyname函式來執行
同理,
785C1D95 PUSH 785C65AC
785C1D9A PUSH DWORD PTR [785C6184]
785C1DA0 CALL DWORD PTR GetProcAddress的位址
785C65ACEnumProtocolsA該字串的位址, 785C6184位址存放著LoadLibraryA(“mswsock.dll”)傳回值,因此我們不難發現在此wsock32.dll的函式EnumProtocolsA是透過mswsock.dll中的EnumProtocolsA所完成的
由這些反組譯的資料中,我們發現在Windows 98wsock32.dll是俱備了轉運的工作,其實真正處理的工作是落在ws2_32.dllmswsock.dll,我想如此的安排主要應該是為了對舊有應用程式的相容性,Windows98中真正工作的是新版的WinSock 2(透過ws2_32.dll),而使用之前版本的WinSock 1.1(透過wsock32.dll)的應用程式則可以在不經修改原始碼的情況下,透過wsock32.dll的轉運來享受到新的版本所帶來的好處,當然如果直接透過ws2_32.dll來寫作程式,則可以免去這份轉運的工作
▓結語
其實,要解決類似的問題,方法當然不只這些,主要還是依各位的巧思來加以應用,我並沒有在Windows NT上測試我的程式碼,我主要寫作環境是在Windows 98,不過,我想在Windows NT上應該會有不少問題發生!NT 4.0就不支援TOOLHELP的函式,而在Windows 2000(NT 5.0),將會對此作支援(請參閱Microsoft System Journal November 1997,A Programmer’s Perspective on New System DLL Features in Windows NT 5.0,Matt Pietrek)
我所提供的兩個範例,是在Visual C 6.0 編譯的Win32 Console程式,在程式執行後,每隔10秒鐘便會對目前存在的行程作一次檢查,如果有使用到wsock32.dll便會加以修改,程式啟動後請不要把SPYSOCK.EXE結束掉,如此才可以確保我們所放在2GB以上的SPYDLL.DLL不會被系統移除,一旦SPYDLL.DLL被移除後,其它被我們所修改的行程中程式碼並沒有被改回來,所以它們依舊會指向SPYDLL.DLL,如此在其它行程跳到SPYDLL.DLL所在位址時,而此DLL卻已不在原處了,將會發生一些錯誤,這部份復原的動作,我有點懶得去寫它們….^_^,在此先跟各位說聲Sorry,此外,我在Netterm,Netscape,CuteFTP…等常用的網路工具中測試過我的範例程式碼,都可以順利運作
程式設計最誘人的部份就是在於它可以充滿了無限的想像空間,我相信在其它層面的應用上,一定還會有更多人想出好的方法來解決類似的問題,在此,我僅把我所能想到的技巧提供給各位,如果有任何需要改進的地方,歡迎各位可以給我一些指正,謝謝