win进程虚拟内存数据搜索与修改Word文档格式.docx
《win进程虚拟内存数据搜索与修改Word文档格式.docx》由会员分享,可在线阅读,更多相关《win进程虚拟内存数据搜索与修改Word文档格式.docx(6页珍藏版)》请在冰豆网上搜索。
一个进程的低2GB有空间的分布如下表:
范围
大小
作用
0x0~~0xFFFF
64KB
不可访问区域,只是用来防止非法的指针访问,访问该范围的地址会导致访问违例。
0x10000~~
0x7FFEFFFF
2GB减去至少192KB
进程的私有地址空间
0x7FFDE000~~
0x7FFDEFFF
4KB
进程中第一个线程的线程环境块,即TEB(Threadenvironmentblock)
0x7FFDF000~~0x7FFDFFFF
进程的进程环境块,即PEB(Processenvironmentblock)
0x7FFE0000~~
0x7FFE0FFF
一个共享的只读用户数据块,该块映射到到系统空间的一个数据块,其中存放的是一些系统信息如系统时间、时钟的滴答数、系统版本号等。
这样访问这些信息的时候系统就不用切换到核心模式。
0x7FFE1000~~
60KB
不可访问
0x7FFF0000~~0x7FFFFFFF
不可访问,用于防止线程的缓冲跨越两种模式空间的边界
表1
二.实现
从上表可以看出,我们要扫描范围的起点和终点不是从0~~2GB,而只是其中的一部分。
要得到这个起点和终点可以使用API函数GetSystemInfo,函数的原型如下:
VOIDGetSystemInfo(
LPSYSTEM_INFOlpSystemInfo
//systeminformation
);
而在结构SYSTEM_INFO中有两个域:
lpMinimumApplicationAddress和lpMaximumApplicationAddress(类型都是LPVOID)中,我们就可以得到一个应用程序可用的最小和最大的地址空间。
这样我们就得到了要扫描的地址的起点和终点。
那么是不是这起点和终点间所有的地址都要扫描呢?
并不是这样的,因为一般情况下一个进程是用不着这么大(接近2GB)的地址空间的。
因此一个进程的大部分地址空间都是未用(Free)或是保留(Reserved)的,真正用到的只是那些已提交(Committed)的内存而已。
内存页面可以有三种状态:
未用(Free)、保留(Reserved)和提交(Committed)。
一个未用的页面是指该页面未被保留或是提交,对一个进程来讲一个未用的页面是不可访问的,访问这样的页面将导致访问违例。
进程可以要求系统保留一些页面以备后用,系统返回一段保留的地址给进程,但是这些地址同样是不可访问的,进程若想使用这段地址空间,使用必须先提交。
只有一个提交的页面才是一个真正可以访问的页面。
不过你提交了一个页面,系统并不会马上分配物理页面,只有在该页面第一次被访问到时,系统才会分配页面并初始化。
另外,这三个状态的两两之间都是可以相互转化的。
相关的API函数有VirtualAlloc、VirtualAllocEx、VirtualFree、VirtualFreeEx等.
这样我们的工作已大大减少了,只需要扫描那些提交的页面就好了。
接下来要做的就是得到一个进程的已提交的页面范围。
这就要用到另外两个API函数VirtualQuery和VirtualQueryEx。
两个函数的功能相似,不同就是VirtualQuery只是查询本进程而VirtualQueryEx可以查询指定进程的内存空间信息,后者正是我们所需要的,函数原型如下:
DWORDVirtualQueryEx(
HANDLEhProcess,
//handletoprocess
LPCVOIDlpAddress,
//addressofregion
PMEMORY_BASIC_INFORMATIONlpBuffer,
//informationbuffer
SIZE_TdwLength
//sizeofbuffer
第一个参数是进程的句柄;
第二个参数是内存地址指针;
第三个参数是指向MEMORY_BASIC_INFORMATION结构的指针,用于返回内存空间的信息;
第四个参数是lpBuffer的长度。
再来看一下结构MEMORY_BASIC_INFORMATION的声明:
typedefstruct_MEMORY_BASIC_INFORMATION{
PVOIDBaseAddress;
PVOIDAllocationBase;
DWORDAllocationProtect;
SIZE_TRegionSize;
DWORDState;
DWORDProtect;
DWORDType;
}MEMORY_BASIC_INFORMATION,*PMEMORY_BASIC_INFORMATION;
第一个参数是查询内存块的基地址;
第二个参数指的是用VirtualAlloc分配该内存时实际分配的基地址,可以小于BaseAddress,也就是说BaseAddress一定包含在AllocationBase分配的范围内;
第三个参数指的是分配该页面时,页面的一些属性,如PAGE_READWRITE、PAGE_EXECUTE等(其它属性可参考PlatformSDK);
第四个参数指的是从BaseAddress开始,具有相同属性的页面的大小。
第五参数指的是页面的状态,有三种可能值:
MEM_COMMIT、MEM_FREE和MEM_RESERVE,这个参数对我们来说是最重要的了,从中我们便可知指定内存页面的状态了;
第六个参数指的是页面的属性,其可能的取值与AllocationProtect相同;
最后一个参数指明了该内存块的类型,有三种可能值:
MEM_IMAGE、MEM_MAPPED和MEM_PRIVATE。
这样我们就可得到进程中需要扫描的地址范围了。
到这里剩下的问题就是要读取指定的进程的指定的地地址空间的内容了。
这里要用到的是用于调试程序和错误处理(DebuggingandErrorHandling)的API函数。
在“PlatformSDK:
DebuggingandErrorHandling”章节中,介绍了一部分与程序调试和错误处理相关的API函数,有许多是很有用,例如我们下面用到的ReadProcessMemory和WriteProcessMemory,它们原型如下:
BOOLReadProcessMemory(
//handletotheprocess
LPCVOIDlpBaseAddress,
//baseofmemoryarea
LPVOIDlpBuffer,
//databuffer
SIZE_TnSize,
//numberofbytestoread
SIZE_T*lpNumberOfBytesRead
//numberofbytesread
BOOLWriteProcessMemory(
LPVOIDlpBaseAddress,
LPCVOIDlpBuffer,
//countofbytestowrite
SIZE_T*lpNumberOfBytesWritten//countofbyteswritten
参数很简单从它们的名字都可以猜出其意义了,这里就不多做说明了。
要说明的是要对一个进程进行ReadProcessMemory操作,当前进程对要读的进程必须有PROCESS_VM_READ访问权。
要对一个进程进行WriteProcessMemory操作,当前进程对要写的进程必须有PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问权。
要获得一个进程的句柄和对这个进程的一些控制权可以使用API函数OpenProcess得到,其使用不做详细说明了,只给出其原型:
HANDLEOpenProcess(
DWORDdwDesiredAccess,
//accessflag
BOOLbInheritHandle,
//handleinheritanceoption
DWORDdwProcessId
//processidentifier
这样对一个进程的用户地址空间内存扫描的流程基本就阐述清楚了。
三
相关的问题:
在实际操作中会遇到一些问题。
如果我们指定了写相关的访问权(如PROCESS_VM_WRITE、PROCESS_SET_INFORMATION、PROCESS_ALL_ACCESS等),用OpenProcess打开一些普通进程是没什么问题,但要是打开的是系统安全进程(如System、Winlogon、smss、csrss、services、lsass等)或是一些注册为服务的进程时,就会遇到“访问拒绝”的错误,这是为了系统的安全而采取的保护手段。
说明了当前的进程没有足够的权限来进行此操作。
在进程控制结构中有一个“访问令牌”(Accesstokens),里面包含有本进程的权限信息。
一些常用的权限如表1所示(摘自InsideWindows2000,ThirdEdition)。
权限名
权限含义
SeBackup
在备份的时候绕过安全检查
SeDebug
可对一个进程进行调试
SeShutdown
可关闭本地系统
SeTakeOwnerShip
在没有得到自由访问权的情况下得到一个对象的所有权
表2
要对一个任意进程(包括系统安全进程和服务进程)进行指定了写相关的访问权的OpenProcess操作,只要当前进程具有SeDeDebug权限就可以了。
要是一个用户是Administrator或是被给予了相应的权限,就可以具有该权限。
可是,就算我们用Administrator帐号对一个系统安全进程执行OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessID)还是会遇到“访问拒绝”的错误。
什么原因呢?
原来在默认的情况下进程的一些访问权限是没有被使能(Enabled)的,所以我们要做的首先是使能这些权限。
与此相关的一些API函数有OpenProcessToken、LookupPrivilegevalue、AdjustTokenPrivileges。
我们要修改一个进程的访问令牌,首先要获得进程访问令牌的句柄,这可以通过OpenProcessToken得到,函数的原型如下:
BOOLOpenProcessToken(
HANDLEProcessHandle,
DWORDDesiredAccess,
PHANDLETokenHandle
);
第一参数是要修改访问权限的进程句柄;
第三个参数就是返回的访问令牌指针;
第二个参数指定你要进行的操作类型,如要修改令牌我们要指定第二个参数为TOKEN_ADJUST_PRIVILEGES(其它一些参数可参考PlatformSDK)。
通过这个函数我们就可以得到当前进程的访问令牌的句柄(指定函数的第一个参数为GetCurrentProcess()就可以了)。
接着我们可以调用AdjustTokenPrivileges对这个访问令牌进行修改。
AdjustTokenPrivileges的原型如下:
BOOLAdjustTokenPrivileges(
HANDLETokenHandle,
//handletotoken
BOOLDisableAllPrivileges,
//disablingoption
PTOKEN_PRIVILEGESNewState,
//privilegeinformation
DWORDBufferLength,
PTOKEN_PRIVILEGESPreviousState,//originalstatebuffer
PDWORDReturnLength
//requiredbuffersize
第一个参数是访问令牌的句柄;
第二个参数决定是进行权限修改还是除能(Disable)所有权限;
第三个参数指明要修改的权限,是一个指向TOKEN_PRIVILEGES结构的指针,该结构包含一个数组,数据组的每个项指明了权限的类型和要进行的操作;
第四个参数是结构PreviousState的长度,如果PreviousState为空,该参数应为NULL;
第五个参数也是一个指向TOKEN_PRIVILEGES结构的指针,存放修改前的访问权限的信息,可空;
最后一个参数为实际PreviousState结构返回的大小。
在使用这个函数前再看一下TOKEN_PRIVILEGES这个结构,其声明如下:
typedefstruct_TOKEN_PRIVILEGES{
DWORDPrivilegeCount;
LUID_AND_ATTRIBUTESPrivileges[];
}TOKEN_PRIVILEGES,*PTOKEN_PRIVILEGES;
PrivilegeCount指的数组原素的个数,接着是一个LUID_AND_ATTRIBUTES类型的数组,再来看一下LUID_AND_ATTRIBUTES这个结构的内容,声明如下:
typedefstruct_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就是指locallyuniqueidentifier,我想GUID大家是比较熟悉的,和GUID的要求保证全局唯一不同,LUID只要保证局部唯一,就是指在系统的每一次运行期间保证是唯一的就可以了。
另外和GUID相同的一点,LUID也是一个64位的值,相信大家都看过GUID那一大串的值,我们要怎么样才能知道一个权限对应的LUID值是多少呢?
这就要用到另外一个API函数LookupPrivilegevalue,其原形如下:
BOOLLookupPrivilegevalue(
LPCTSTRlpSystemName,
//systemname
LPCTSTRlpName,
//privilegename
PLUIDlpLuid
//locallyuniqueidentifier
第一个参数是系统的名称,如果是本地系统只要指明为NULL就可以了,第三个参数就是返回LUID的指针,第二个参数就是指明了权限的名称,如“SeDebugPrivilege”。
在Winnt.h中还定义了一些权限名称的宏,如:
#defineSE_BACKUP_NAME
TEXT("
SeBackupPrivilege"
)
#defineSE_RESTORE_NAME
SeRestorePrivilege"
#defineSE_SHUTDOWN_NAME
SeShutdownPrivilege"
#defineSE_DEBUG_NAME
SeDebugPrivilege"
这样通过这三个函数的调用,我们就可以用OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessID)来打获得任意进程的句柄,并且指定了所有的访问权。
四总结
用户模式的内存扫描还是具有想当的局限性,它不能完全扫描Windows2000的全部内存空间。
要对系统空间进行扫描,在Windows2000下,用户模式的应用程序是不能实现的。
要实现对系统空间的扫描,必须通过工作于核心模式的程序—驱动程序来实现。