目录
一、权限管理
1.1 Windows权限管理
1.1.1 令牌
Windows系统里有专门的权限管理办法,主要靠账户权限来落实。用户用某个账户登录系统后,新创建的进程就会获得和登录账户一样的权限。而这个进程创建的线程,默认和进程的权限相同。由于线程负责执行代码,是真正访问资源的单元,所以权限主要围绕进程或线程展开。在Windows系统里,这种权限被叫做令牌。
我们创建对象时,对象一般都会带有安全描述符。安全描述符会规定哪些用户具备什么权限可以访问这个对象,哪些用户会被拒绝访问。所以,线程能不能访问对象,由线程持有的访问令牌,以及对象的安全描述符共同决定。
我们使用计算机时得登录账户,借此和操作系统建立会话,操作系统会给这个会话分配对应的权限。在Windows操作系统里,权限被抽象成Token,也就是令牌。系统创建进程时,会把对应账户的令牌分配给进程,让进程拥有访问资源的相应权限。访问令牌里存着当前账户的SID,以及该账户拥有的权限 。
1.1.2 访问控制列表
当进程要访问对象时,每个安全对象都配有安全描述符。安全描述符会讲清楚哪些用户能访问这个对象,哪些用户不能访问。
安全描述符里有访问控制列表。在访问控制列表(ACL)中,通过一个个访问控制入口(ACE),就能确定每个账户具备什么权限可以访问对象,哪些账户会被禁止访问。
1.2 UAC
在Windows Vista系统发布以后,Windows操作系统加入了UAC机制,它的全称是User Account Control,也就是用户账户控制。就算你用管理员账号登录系统,在创建进程时,系统还是会给这个进程分配一个低权限的令牌。这种令牌还有个名字,叫Filter Token,也就是过滤令牌。
只有当程序以管理员身份启动运行,系统才会给它高权限。不过,只要是以管理员身份运行的程序,都会弹出一个选择对话框。
在使用软件过程中,除了启动程序时会触发UAC控制,我们还能看到一些按钮带有UAC样式的图标。这种机制允许针对软件的个别特殊功能进行提权操作,大大提升了软件使用的便捷性。用户不用每次打开软件都被UAC提示框打扰,而是能按照实际需求,决定是否给软件更高的运行权限。
要实现这一机制,既需要依靠复杂的权限控制系统,也离不开特定的进程创建逻辑:
1. 软件运行时,会先判断当前进程的运行权限。
2. 要是进程以低权限运行,软件就会给那些需要提权操作的按钮加上UAC盾牌标志。
3. 要是进程已经以高权限运行,软件就会隐藏提权按钮。
4. 当用户点击提权按钮,软件会通过ShellExecuteEx()函数,以管理员权限重新启动自身进程 。
显示UAC提升按钮的示例代码
//1.获得本进程的令牌
HANDLE hToken =NULL;
if(!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY,&hToken))
return false;
//2.获取提升类型
TOKEN_ELEVATION_TYPE ElevationType =TokenElevationTypeDefault;
BOOL bIsAdmin =false;
DWORD dwSize =0;
if(GetTokenInformation(hToken,TokenElevationType,&ElevationType,
sizeof(TOKEN_ELEVATION_TYPE),&dwSize)) // 返回大小
{
//2.1创建管理员组的对应SID
BYTE adminSID[SECURITY_MAX_SID_SIZE];
dwSize=sizeof(adminSID);
CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, &adminSID, &dwSize);
//2.2判断当前进程运行用户角色是否为管理员
if(ElevationType ==TokenElevationTypeLimited){
//a.获取连接令牌的句柄
HANDLE hUnfilteredToken =NULL;
GetTokenInformation(hToken, TokenLinkedToken,
(PVOID)&hUnfilteredToken,
sizeof(HANDLE),&dwSize);
//b.检查这个原始的令牌是否包含管理员的SID
if(!CheckTokenMembership(hUnfilteredToken,&adminSID,&bIsAdmin))
return false;
CloseHandle(hUnfilteredToken);
}else {
bIsAdmin =IsUserAnAdmin();
}
}
CloseHandle(hToken);
//3.判断具体的权限状况
BOOL bFullToken =false;
switch(ElevationType){
case TokenElevationTypeDefault:/*默认的用户或UAC被禁用*/
if(IsUserAnAdmin())
bFullToken=true;//默认用户有管理员权限
else
bFullToken=false;//默认用户不是管理员组
break;
case TokenElevationTypeFull: /*已经成功提高进程权限*/
if(IsUserAnAdmin())
bFullToken=true;//当前以管理员权限运行
else
bFullToken=false;//当前未以管理员权限运行
break;
case TokenElevationTypeLimited:/*进程在以有限的权限运行*/
if(bIsAdmin)
bFullToken=false;//用户有管理员权限,但进程权限有限
else
bFullToken=false;//用户不是管理员组,且进程权限有限
}
//4.根据权限的不同控制按钮的显示
if(!bFullToken)
Button_SetElevationRequiredState(GetDlgItem(hWnd,控件ID),!bFullToken);
else
ShowWindow(GetDlgItem(hWnd,控件ID),SW_HIDE);
以管理员权限打开进程的代码
//1.隐藏当前窗口
ShowWindow(hWnd,SW_HIDE);
//2.获取当前程序路径
WCHAR szApplication[MAX_PATH]={0};
DWORD cchLength =_countof(szApplication);
QueryFullProcessImageName(GetCurrentProcess(),0,
szApplication,&cchLength );
//3.以管理员权限重新打开进程
SHELLEXECUTEINFO sei ={sizeof(SHELLEXECUTEINFO)};
sei.lpVerb =L"runas"; //请求提升权限
sei.lpFile =szApplication;//可执行文件路径
sei.lpParameters =NULL; //不需要参数
sei.nShow =SW_SHOWNORMAL;//正常显示窗口
if(ShellExecuteEx(&sei))
ExitProcess(0);
else
ShowWindow(hWnd,SW_SHOWNORMAL);
提升当前进程权限为调试权限的代码
//提升为调试权限
BOOL EnableDebugPrivilege(BOOL fEnable){
BOOL fOk =FALSE;
HANDLE hToken;
//以修改权限的方式,打开进程的令牌
if(OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES, &hToken)){
//令牌权限结构体
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount =1;
//获得LUID
LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = fEnable?SE_PRIVILEGE_ENABLED:0;
//修改权限
AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(tp),NULL,NULL);
fOk =(GetLastError()==ERROR_SUCCESS);
CloseHandle(hToken);
}
return(fOk);
}
二、内存管理
2.1 虚拟内存
内存是一种硬件设备,它的读写速度比硬盘快不少。CPU运行速度特别快,需要有高速设备跟它进行数据交换,这个任务就由内存来承担。我们可以把内存看成计算机运行时的数据中转站。现在CPU性能普遍很强,增加内存容量、提高内存频率,能有效提升计算机的整体性能。
计算机运行程序离不开内存,当前正在运行的所有程序,操作系统内核的代码和数据,还有一部分资源,都存放在内存当中。这样一来,如何合理管理内存就成了难题。理想的内存管理应该满足下面这些要求:
1. 任何一个进程都不能随便访问其他进程的内存。
2. 当进程需要更多内存时,系统能动态分配,灵活性要好。
3. 每个进程的内存管理方式得统一,便于程序员使用。
4. 内存要有不同的属性,不同属性的内存执行不同的任务。
5. 程序员使用内存时,不用去了解这些底层设计。
为了解决上述以及其他相关问题,Windows系统引入了虚拟内存的概念。
不管实际的物理内存有多大,每个进程都有4GB的虚拟内存空间。每个进程对虚拟内存空间的使用方法差不多,低2GB是用户空间,高2GB是系统空间,低2GB用户代码空间的代码没办法访问高2GB系统空间。
进程里使用的都是虚拟地址,虚拟地址到物理地址的转换由操作系统内核负责。所以,在自己的进程里,是访问不了其他进程内存的,尽管不同进程的地址看上去很像。
一个进程的虚拟空间,只有一部分和物理内存有映射关系。Windows系统会尽量保证,不同进程用到的同一份数据,在物理内存里只存一份,然后分别映射到多个进程中,借此节省内存。
当各个进程使用的内存总量超过了物理内存,操作系统会把物理内存中暂时用不到的数据,转移到硬盘上。
虚拟内存到物理内存的转换,主要通过分段与分页机制实现(以分页为主)。这部分内容将在后面讲解,本节课主要熟悉用户模式下内存相关API的使用。
2.2 堆内存的管理
2.2.1 Windows中的堆
在Windows系统里,堆被当作对象来管理。我们能创建堆,在堆上分配内存,也能销毁堆里的内存。C/C++语言里的new和malloc函数,说到底也是依靠Windows系统的堆对象来分配内存空间的。
Windows系统在创建进程时,会给这个进程创建一个默认堆,这个默认堆没办法被销毁。有些时候,在特定的时间段或者特定任务里,会需要大量内存,而且可能是很多小碎片状的内存块。要是都用默认堆,等这些内存都用完了,释放起来就特别麻烦,得一个一个地释放。要是任务执行过程中出了异常,还可能导致部分内存碎片的句柄丢失,释放难度就更大了,因为没法把它们一个个找出来释放,这样就很容易造成内存丢失。遇到这种情况,我们可以用HeapCreate函数创建一个独立的内存堆,等使用完之后,直接用HeapDestroy函数,就能把这个内存堆里的所有内存都释放掉。
2.2.2 涉及到的函数
使用示例
1. 创建一个堆使用
HANDLE hHeap =HeapCreate(HEAP_NO_SERIALIZE,0,0);
SYSTEM_INFO si; //系统信息
GetSystemInfo(&si);//获取系统信息
//在堆上分配3个页面大小的内存
lpMem =HeapAlloc(hHeap,HEAP_ZERO_MEMORY,si.dwPageSize*3);
HeapFree(hHeap,HEAP_NO_SERIALIZE,lpMem);
HeapDestroy(hHeap);
2. 在已经存在的堆上申请空间
HANDLE hHeap =GetProcessHeap();//获取已存在的堆
SYSTEM_INFO si; //系统信息
GetSystemInfo(&si); //获取系统信息
//在堆上分配3个页面大小的内存
lpMem =HeapAlloc(hHeap,HEAP_ZERO_MEMORY,si.dwPageSize*3);
HeapFree(hHeap,HEAP_NO_SERIALIZE,lpMem);
HeapDestroy(hHeap);
2.3 虚拟内存的管理
虚拟内存按分页管理,目前一个内存页大小为4KB,因此管理内存时,通常以4KB为单位。管理虚拟内存需要用到以下函数:
2.3.1 申请与释放虚拟内存
申请一块虚拟内存
VirtualAlloc(
_In_opt_LPVOID lpAddress,
_In_SIZE_T dwSize,
_In_DWORD flAllocationType,
_In_DWORD flProtect
);
- 参数1:分配的起始位置,函数会自动将其对齐到整数位置。
- 参数2:要分配的内存区域大小。
- 参数3:指定这块内存是预定还是提交。
- 参数4:内存的保护属性。
BOOL
WINAPI
VirtualFree(
LPVOID lpAddress,
_In_SIZE_T dwSize,
_In_DWORD dwFreeType
);
- 参数1:需要改变状态的内存区域的起始地址。
- 参数2:需要改变状态的大小。
- 参数3:设置为MEM_DECOMMIT,将内存变为保留状态,当dwSize为0时,参数1必须为VirtualAlloc得到的申请好的内存的起始地址;设置为MEM_RELEASE,释放内存,将内存变为空闲状态。
虚拟内存有三种状态:
当成功创建一个进程内核对象后,系统内核会给它划分出一部分物理内存,同时创建一块虚拟内存。新创建的虚拟内存刚开始只是在逻辑层面存在,还没有和物理内存建立映射关系,这个时候的内存被叫做闲置(Free)内存,或者未分配(unallocated)内存。
要是想用这些内存,就得调用VirtualAlloc()函数,从里面划出一块内存区域,这种划区域的操作就叫预定。
系统在分配内存空间的时候,会保证分配的内存起始地址,是内存分配粒度的整数倍。在X86架构的系统里,内存分配粒度是64KB。
虚拟内存空间里,大部分区域都是空闲的,已经提交能使用的内存,就好比是浩瀚大海里零散分布的几块陆地。所以在操作虚拟内存时,要是不注意,就容易出错。
示例代码
LPVOID lpvBase=VirtualAlloc(
NULL,
1024*64*5, //64KB*5
MEM_RESERVE, //预定内存区域
PAGE_NOACCESS);//不可访问
LPVOID lpvResult=VirtualAlloc(
lpvBase,
1024 *64 *1, //64KB*1
MEM_COMMIT, //调拨内存区域
PAGE_READWRITE);//可读写
StringCchCopy((LPWSTR)lpvResult,_countof (L"hello!"),L"hello!");
MessageBox(NULL,(LPWSTR)lpvResult,NULL,MB_OK);
2.3.2 虚拟内存的安全属性
每个内存页都有自己的访问属性:
虚拟内存的属性可通过VirtualProtect进行修改。
2.3.3 在其他进程中分配虚拟内存,读取和修改虚拟内存
VirtualAllocEx、ReadProcessMemory与WriteProcessMemory三个函数,可实现跨进程的内存分配、读取、写入等操作,是很多安全技术的基础函数,后续学习远程线程注入时,会详细讲解这些函数的用法。
2.4 内存映射
文件映射(Mapping)是将文件内容映射到进程虚拟内存中的技术。映射成功的文件可以用视图(View)引用这段内存,从而实现对该段内存中文件的操作。
使用文件映射时,需先创建映射对象,映射对象分为命名和未命名两种,命名的映射对象可进行跨进程读写。
文件映射的作用及优势:
1. 简化文件操作。
2. 文件仍在硬盘中,映射视图是一段内存,效率高。
3. 可在不同进程间共享数据。
示例代码
HANDLE hFile; //文件句柄
HANDLE hMapFile; //文件内存映射区域的句柄
LPVOID lpMapAddress;//内存映射区域的起始位置
//1.创建一个文件
hFile = CreateFile(L"D:\\xxxxx", GENERIC_READ | GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (INVALID_HANDLE_VALUE == hFile) return FALSE;
//2.创建文件映射
hMapFile = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0,
GetFileSize(hFile, NULL), NULL);
if (NULL == hMapFile) return FALSE;
//3.将文件映射View
lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if (NULL == lpMapAddress) return FALSE;
//......
//可以使用lpMapAddress进行一些操作
//......
//4.将映射的数据写回到硬盘上
FlushViewOfFile(lpMapAddress, 0);
//5.关闭mapping对象
if (!CloseHandle(hMapFile)) return FALSE;
if (!CloseHandle(hFile)) return FALSE;
进程间通讯示例代码
进程A
//1.创建命名的文件映射
HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0,
16, L"File_Mapping_Test");
if (NULL == hMapFile || INVALID_HANDLE_VALUE == hMapFile)
return FALSE;
//2.创建View
PVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 16);
if (NULL == pBuf)
return FALSE;
//3.将共享数据复制到文件映射中
wcscpy_s((PWCHAR)pBuf, 6, L"测试文本");
//4.循环等待
while (*((PBYTE)pBuf))
Sleep(200);
//5.取消Mapping,关闭句柄
UnmapViewOfFile(pBuf);
CloseHandle(hMapFile);
进程B
//1.打开文件Mapping
HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"File_Mapping_Test");
if (NULL == hMapFile || INVALID_HANDLE_VALUE == hMapFile)
return FALSE;
//2.创建View
PVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 16);
if (NULL == pBuf)
return FALSE;
//3.显示共享数据
MessageBox(NULL, (LPWSTR)pBuf, L"FileMapping", MB_OK);
//4.修改共享数据
*((PBYTE)pBuf) = 0;
//5.取消Mapping,关闭句柄
UnmapViewOfFile(pBuf);
CloseHandle(hMapFile);
2.5 虚拟内存遍历
我们可通过系统提供的API函数VirtualQueryEx()获取某进程的虚拟内存分布状态,其原型如下:
SIZE_T WINAPI VirtualQueryEx(
_In_ HANDLE hProcess, //进程句柄
_In_opt_ LPCVOID lpAddress,//查询地址
_Out_ PMEMORY_BASIC_INFORMATION lpBuffer,//内存的信息
_In_ SIZE_T dwLength //传出结构体的大小
);
此函数执行后会返回一个MEMORY_BASIC_INFORMATION结构体,里面包含该内存地址的详细信息。
typedef struct _MEMORY_BASIC_INFORMATION{
PVOID BaseAddress; //区域地址,此区域包含传入地址
PVOID AllocationBase; //将参数向下取整到页面大小
DWORD AllocationProtect; //此区域在预定时的保护属性
SIZE_T RegionSize; //区域的大小
DWORD State; //区域的页面状态[注1]
DWORD Protect; //页面保护属性
DWORD Type; //页面类型[注2]
}MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
注1:内存页面有三种状态,分别是MEM_FREE(闲置)、MEM_RESERVE(预订)、MEM_COMMIT(调拨)。要是页面状态是MEM_FREE,那么AllocationBase、AllocationProtect、State、Protect这几个值都没有意义。要是页面状态是MEM_RESERVE,Protect这个值就没有意义。
注2:内存页面有MEM_IMAGE、MEM_MAPPED、MEM_PRIVATE这三种类型 。