Windows2000下用戶模式的內存掃描[轉帖]

老文1篇了,複習複習,前幾天寫的快樂生活快樂遊戲的那個工具,就是差不太多原理的

Windows2000下用戶模式的內存掃描[轉帖]
        Sprite

簡述:

    本文簡要介紹了在Windows2000下實現內存掃描的基本理論和實現的辦法。內存掃描是一項重要的技術,有相當廣泛的應用範圍:如病毒掃描、遊戲修改等。Windows2000是一個完全保護的系統,且具有兩種工作模式,即用戶態和核心態(User Model and Kernel Model)。內存掃描也可分爲用戶態的內存掃描與核心態的內存掃描。本文主要講述的是工作於用戶態的內存掃描。
一.相關理論

   早期在DOS壞境下進行內存掃描是一件相對簡單的事情。因爲DOS工作在CPU的實模式下,沒有采用虛存技術也沒有提供內存的保護機制,只要實實在在的掃描完所有的物理內存,一切工作也就完成了,早期有一些防毒軟件就是用了這樣的辦法。當然爲了提高效率,我們並不用掃描所有的內存區域,因爲有些空間是沒有被用到的,掃描這些地方也是隻浪費時間。這可以通過遍歷DOS系統的MCB(Memory Control Block)鏈,來得到實際內存的使用區域,從而使掃描的效率大大提高。相似的思路在Windows2000下的內存掃描也是適用的。

Windows2000則是一個完全保護的系統,工作於CPU的保護模式下,引入了虛存技術。每個進程擁有獨立的4GB的地址空間,其中低的2GB爲進程的私有空間,高的2GB爲系統空間的映射(如果在Boot.ini文件中使用“/3GB”的開關可以使進程的私有空間增大到3GB,系統空間1GB)。對於每個進程來講其虛擬的地址空間是連續的,實際上它們是以頁面爲單位離散的存在於物理內存中,一些可能被交換到硬盤上的頁面文件中,而且還有大部分的空間是未提交(Uncommitted)的。因此在Windows2000中對進程的用戶空間進行掃描必須依次對每個進程的空間進行掃描。一個進程的低2GB有空間的分佈如下表:

範圍
 大小
 作用
 
0x0~~ 0xFFFF
 64 KB
 不可訪問區域,只是用來防止非法的指針訪問,訪問該範圍的地址會導致訪問違例。
 
0x10000~~

0x7FFEFFFF
 2 GB 減去至少192 KB
 進程的私有地址空間
 
0x7FFDE000~~

0x7FFDEFFF
 4 KB
 進程中第一個線程的線程環境塊,即TEB(Thread environment block)
 
0x7FFDF000~~ 0x7FFDFFFF
 4 KB
 進程的進程環境塊,即PEB(Process environment block)


 
0x7FFE0000~~

0x7FFE0FFF
 4 KB
 一個共享的只讀用戶數據塊,該塊映射到到系統空間的一個數據塊,其中存放的是一些系統信息如系統時間、時鐘的滴答數、系統版本號等。這樣訪問這些信息的時候系統就不用切換到核心模式。
 
0x7FFE1000~~

0x7FFEFFFF
 60 KB
 不可訪問
 
0x7FFF0000~~ 0x7FFFFFFF
 64 KB
 不可訪問,用於防止線程的緩衝跨越兩種模式空間的邊界
表1


二.實現
從上表可以看出,我們要掃描範圍的起點和終點不是從0~~2GB,而只是其中的一部分。要得到這個起點和終點可以使用API函數GetSystemInfo,函數的原型如下:
VOID GetSystemInfo(
  LPSYSTEM_INFO lpSystemInfo  // system information
);
   而在結構SYSTEM_INFO中有兩個域:lpMinimumApplicationAddress和 lpMaximumApplicationAddress(類型都是LPVOID) 中,我們就可以得到一個應用程序可用的最小和最大的地址空間。這樣我們就得到了要掃描的地址的起點和終點。那麼是不是這起點和終點間所有的地址都要掃描呢?並不是這樣的,因爲一般情況下一個進程是用不着這麼大(接近2GB)的地址空間的。因此一個進程的大部分地址空間都是未用(Free)或是保留(Reserved)的,真正用到的只是那些已提交(Committed)的內存而已。
     內存頁面可以有三種狀態:未用(Free)、保留(Reserved)和提交(Committed)。一個未用的頁面是指該頁面未被保留或是提交,對一個進程來講一個未用的頁面是不可訪問的,訪問這樣的頁面將導致訪問違例。進程可以要求系統保留一些頁面以備後用,系統返回一段保留的地址給進程,但是這些地址同樣是不可訪問的,進程若想使用這段地址空間,使用必須先提交。只有一個提交的頁面纔是一個真正可以訪問的頁面。不過你提交了一個頁面,系統並不會馬上分配物理頁面,只有在該頁面第一次被訪問到時,系統纔會分配頁面並初始化。另外,這三個狀態的兩兩之間都是可以相互轉化的。相關的API函數有VirtualAlloc、VirtualAllocEx、VirtualFree、VirtualFreeEx等.
這樣我們的工作已大大減少了,只需要掃描那些提交的頁面就好了。接下來要做的就是得到一個進程的已提交的頁面範圍。這就要用到另外兩個API函數VirtualQuery和VirtualQueryEx。兩個函數的功能相似,不同就是VirtualQuery只是查詢本進程而VirtualQueryEx可以查詢指定進程的內存空間信息,後者正是我們所需要的,函數原型如下:
DWORD VirtualQueryEx(
    HANDLE hProcess,                     // handle to process
    LPCVOID lpAddress,                   // address of region
    PMEMORY_BASIC_INFORMATION lpBuffer,  // information buffer
    SIZE_T dwLength                      // size of buffer
);
第一個參數是進程的句柄;第二個參數是內存地址指針;第三個參數是指向MEMORY_BASIC_INFORMATION結構的指針,用於返回內存空間的信息;第四個參數是lpBuffer的長度。再來看一下結構MEMORY_BASIC_INFORMATION的聲明:
typedef struct _MEMORY_BASIC_INFORMATION {
     PVOID BaseAddress;
     PVOID AllocationBase;
     DWORD AllocationProtect;
     SIZE_T RegionSize;
     DWORD State;
     DWORD Protect;
     DWORD Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
    第一個參數是查詢內存塊的基地址;第二個參數指的是用VirtualAlloc分配該內存時實際分配的基地址,可以小於BaseAddress,也就是說BaseAddress一定包含在AllocationBase分配的範圍內;第三個參數指的是分配該頁面時,頁面的一些屬性,如PAGE_READWRITE、PAGE_EXECUTE等(其它屬性可參考Platform SDK);第四個參數指的是從BaseAddress開始,具有相同屬性的頁面的大小。第五參數指的是頁面的狀態,有三種可能值:MEM_COMMIT、MEM_FREE和MEM_RESERVE,這個參數對我們來說是最重要的了,從中我們便可知指定內存頁面的狀態了;第六個參數指的是頁面的屬性,其可能的取值與AllocationProtect相同;最後一個參數指明瞭該內存塊的類型,有三種可能值:MEM_IMAGE 、MEM_MAPPED和MEM_PRIVATE。
 這樣我們就可得到進程中需要掃描的地址範圍了。到這裏剩下的問題就是要讀取指定的進程的指定的地地址空間的內容了。這裏要用到的是用於調試程序和錯誤處理(Debugging and Error Handling)的API函數。在“Platform SDK: Debugging and Error Handling”章節中,介紹了一部分與程序調試和錯誤處理相關的API函數,有許多是很有用,例如我們下面用到的ReadProcessMemory和WriteProcessMemory,它們原型如下:
BOOL ReadProcessMemory(
     HANDLE hProcess,              // handle to the process
     LPCVOID lpBaseAddress,        // base of memory area
     LPVOID lpBuffer,              // data buffer
     SIZE_T nSize,                 // number of bytes to read
     SIZE_T * lpNumberOfBytesRead  // number of bytes read
);
BOOL WriteProcessMemory(
     HANDLE hProcess,                // handle to process
     LPVOID lpBaseAddress,           // base of memory area
     LPCVOID lpBuffer,               // data buffer
     SIZE_T nSize,                   // count of bytes to write
     SIZE_T * lpNumberOfBytesWritten // count of bytes written
);
參數很簡單從它們的名字都可以猜出其意義了,這裏就不多做說明了。要說明的是要對一個進程進行ReadProcessMemory操作,當前進程對要讀的進程必須有PROCESS_VM_READ訪問權。要對一個進程進行WriteProcessMemory操作,當前進程對要寫的進程必須有PROCESS_VM_WRITE 和PROCESS_VM_OPERATION訪問權。要獲得一個進程的句柄和對這個進程的一些控制權可以使用API函數OpenProcess得到,其使用不做詳細說明了,只給出其原型:
HANDLE OpenProcess(
     DWORD dwDesiredAccess,  // access flag
     BOOL bInheritHandle,    // handle inheritance option
     DWORD dwProcessId       // process identifier
);
 

 這樣對一個進程的用戶地址空間內存掃描的流程基本就闡述清楚了。

三  相關的問題:

        在實際操作中會遇到一些問題。如果我們指定了寫相關的訪問權(如PROCESS_VM_WRITE、PROCESS_SET_INFORMATION、PROCESS_ALL_ACCESS等),用OpenProcess打開一些普通進程是沒什麼問題,但要是打開的是系統安全進程(如System、Winlogon、smss、csrss、services、lsass等)或是一些註冊爲服務的進程時,就會遇到“訪問拒絕”的錯誤,這是爲了系統的安全而採取的保護手段。說明了當前的進程沒有足夠的權限來進行此操作。在進程控制結構中有一個“訪問令牌”(Access tokens),裏面包含有本進程的權限信息。一些常用的權限如表1所示(摘自Inside Windows2000,Third Edition)。

  權限名
       權限含義
 
SeBackup
 在備份的時候繞過安全檢查
 
SeDebug
 可對一個進程進行調試
 
SeShutdown
 可關閉本地系統
 
SeTakeOwnerShip
 在沒有得到自由訪問權的情況下得到一個對象的所有權
 

 

表2  

要對一個任意進程(包括系統安全進程和服務進程)進行指定了寫相關的訪問權的OpenProcess操作,只要當前進程具有SeDeDebug權限就可以了。要是一個用戶是Administrator或是被給予了相應的權限,就可以具有該權限。可是,就算我們用Administrator帳號對一個系統安全進程執行OpenProcess(PROCESS_ALL_ACCESS,FALSE, dwProcessID)還是會遇到“訪問拒絕”的錯誤。什麼原因呢?原來在默認的情況下進程的一些訪問權限是沒有被使能(Enabled)的,所以我們要做的首先是使能這些權限。與此相關的一些API函數有OpenProcessToken、LookupPrivilegevalue、AdjustTokenPrivileges。我們要修改一個進程的訪問令牌,首先要獲得進程訪問令牌的句柄,這可以通過OpenProcessToken得到,函數的原型如下:

BOOL OpenProcessToken(
     HANDLE ProcessHandle,
     DWORD DesiredAccess,
PHANDLE TokenHandle
 );
第一參數是要修改訪問權限的進程句柄;第三個參數就是返回的訪問令牌指針;第二個參數指定你要進行的操作類型,如要修改令牌我們要指定第二個參數爲TOKEN_ADJUST_PRIVILEGES(其它一些參數可參考Platform SDK)。通過這個函數我們就可以得到當前進程的訪問令牌的句柄(指定函數的第一個參數爲GetCurrentProcess()就可以了)。接着我們可以調用AdjustTokenPrivileges對這個訪問令牌進行修改。AdjustTokenPrivileges的原型如下:
BOOL AdjustTokenPrivileges(
     HANDLE TokenHandle,              // handle to token
     BOOL DisableAllPrivileges,       // disabling option
     PTOKEN_PRIVILEGES NewState,      // privilege information
     DWORD BufferLength,              // size of buffer
     PTOKEN_PRIVILEGES PreviousState, // original state buffer
     PDWORD ReturnLength              // required buffer size
);
第一個參數是訪問令牌的句柄;第二個參數決定是進行權限修改還是除能(Disable)所有權限;第三個參數指明要修改的權限,是一個指向TOKEN_PRIVILEGES結構的指針,該結構包含一個數組,數據組的每個項指明瞭權限的類型和要進行的操作; 第四個參數是結構PreviousState的長度,如果PreviousState爲空,該參數應爲NULL;第五個參數也是一個指向TOKEN_PRIVILEGES結構的指針,存放修改前的訪問權限的信息,可空;最後一個參數爲實際PreviousState結構返回的大小。在使用這個函數前再看一下TOKEN_PRIVILEGES這個結構,其聲明如下:

typedef struct _TOKEN_PRIVILEGES {
     DWORD PrivilegeCount;
     LUID_AND_ATTRIBUTES Privileges[];
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
PrivilegeCount指的數組原素的個數,接着是一個LUID_AND_ATTRIBUTES類型的數組,再來看一下LUID_AND_ATTRIBUTES這個結構的內容,聲明如下:

typedef struct _LUID_AND_ATTRIBUTES {
     LUID   Luid;
     DWORD  Attributes;
} LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES

第二個參數就指明瞭我們要進行的操作類型,有三個可選項: SE_PRIVILEGE_ENABLED、SE_PRIVILEGE_ENABLED_BY_DEFAULT、SE_PRIVILEGE_USED_FOR_ACCESS。要使能一個權限就指定Attributes爲SE_PRIVILEGE_ENABLED。第一個參數就是指權限的類型,是一個LUID的值,LUID就是指locally unique identifier,我想GUID大家是比較熟悉的,和GUID的要求保證全局唯一不同,LUID只要保證局部唯一,就是指在系統的每一次運行期間保證是唯一的就可以了。另外和GUID相同的一點,LUID也是一個64位的值,相信大家都看過GUID那一大串的值,我們要怎麼樣才能知道一個權限對應的LUID值是多少呢?這就要用到另外一個API函數LookupPrivilegevalue,其原形如下:

BOOL LookupPrivilegevalue(
     LPCTSTR lpSystemName,  // system name
     LPCTSTR lpName,        // privilege name
     PLUID lpLuid           // locally unique identifier
);
第一個參數是系統的名稱,如果是本地系統只要指明爲NULL就可以了,第三個參數就是返回LUID的指針,第二個參數就是指明瞭權限的名稱,如“SeDebugPrivilege”。在Winnt.h中還定義了一些權限名稱的宏,如:

#define SE_BACKUP_NAME        TEXT("SeBackupPrivilege")

#define SE_RESTORE_NAME       TEXT("SeRestorePrivilege")

#define SE_SHUTDOWN_NAME      TEXT("SeShutdownPrivilege")

#define SE_DEBUG_NAME         TEXT("SeDebugPrivilege")

這樣通過這三個函數的調用,我們就可以用OpenProcess(PROCESS_ALL_ACCESS,FALSE, dwProcessID)來打獲得任意進程的句柄,並且指定了所有的訪問權。
四 總結

    用戶模式的內存掃描還是具有想當的侷限性,它不能完全掃描Windows2000的全部內存空間。要對系統空間進行掃描,在Windows2000下,用戶模式的應用程序是不能實現的。要實現對系統空間的掃描,必須通過工作於核心模式的程序—驅動程序來實現。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章