原文地址:http://www.freebuf.com/articles/4546.html
Author: Pnig0s[FreeBuf]
阅读本文的朋友需要对Windows访问控制模型有初步的了解,了解Token(访问令牌),ACL(访问控制列表),DACL(选择访问控制列表),ACE(访问控制列表项)等与访问控制模型相关的名词含义及之间的关系,当然我也会在文中简要科普一下ACM。
写这篇文章的目的主要是最近在写一个Win下本地提权的东西,涉及到了对ACL的操作,以前对ACL总是避而远之,Windows访问控制模型比较头疼,一个API会牵出一大把要用的API。毕竟涉及到用户访问的安全,肯定不能让编程人员随意更改这些机制,复杂一些也可以理解,可是能参考的资料很少,MSDN上关于一些访问控制相关API的使用和结构体的描述也含糊不清也没有什么代码实例。这篇文章也是在查阅国外了一些文献加上自己研究测试后完成的,发出来希望对涉及这方面编程的朋友有帮助。
-----》》熟悉Windows访问控制机制的可以跳过本段:
因为是科普我这里简单介绍下Windows访问控制模型(ACM),别嫌我啰嗦,懂得直接Pass往下看。ACM中最重要的两部分是访问令牌(Access Token)和安全描述符表(Security Descriptor)。访问令牌存在于访问主体中,安全描述符表存在于访问客体中。比如我去米国,我就是访问主体,米国就是访问客体,我持有的签证就是访问令牌。系统中访问主体是进程客体是一切系统对象。访问令牌中有当前用户的唯一标识SID,组唯一标识SID以及一些权限标志(Privilege)。安全描述符表(SD)存在于Windows系统中的任何对象中(文件,注册表,互斥量,信号量等等)。SD中包含对象所有者的SID,组SID以及两个非常重要的数据结构选择访问控制列表(DACL)和系统访问控制列表(SACL),其中SACL涉及系统日志用的很少可以先无视。DACL中包含一个个ACE访问控制入口也是权限访问判断的核心,当一个进程访问某一对象的时候,对象会将进程的Token与自身的ACE依次比对,直到被允许或被拒绝,前面的ACE优于后面的ACE。整体的一个权限检查过程如下图:
-----》》
BOOL WINAPI GetFileSecurity(
__in LPCTSTR lpFileName,
__in SECURITY_INFORMATION RequestedInformation,
__out_opt PSECURITY_DESCRIPTOR pSecurityDescriptor,
__in DWORD nLength,
__out LPDWORD lpnLengthNeeded
);
lpFileName指定了要获取SD的文件。首先要定义一个PSECURITY_DESCRIPTOR的安全描述符表指针,因为描述符表大小未知,所以要调用两次GetFileSecurity()第一次将nLength置0,函数会返回实际大小,然后第二次用获取的大小去接收完整的SD,代码如下:
文件开始部分定义的内存分配释放函数常量:
#define AllocMem(x) (HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,x))
#define FreeMem(x) (HeapFree(GetProcessHeap(),HEAP_ZERO_MEMORY,x))
...
...
BOOL bRs = FALSE;
DWORD dwSizeNeeded = 0;
PSECURITY_DESCRIPTOR psd = NULL;
SECURITY_INFORMATION si = OWNER_SECURITY_INFORMATION
| GROUP_SECURITY_INFORMATION|DACL_SECURITY_INFORMATION;
bRs = GetFileSecurity(lpFileName,si,psd,0,&dwSizeNeeded);
//第一次调用获得SD实际大小
if(!bRs)
{
if(GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
psd = (PSECURITY_DESCRIPTOR)AllocMem(dwSizeNeeded);
//根据获取到的大小对psd分配内存
}else
{
printf("\n[-]Get SD failed:%d",GetLastError());
return bRs;
}
} if(!GetFileSecurity(lpFileName,si,psd,dwSizeNeeded,&dwSizeNeeded))
{
printf("\n[-]Get SD failed:%d",GetLastError());
return bRs;
}
至此针对指定文件对象的安全描述符表已经得到,下一步需要提取出访问进程的访问令牌(Token)。首先调用OpenProcessToken()获得本进程的Token,参数比较简单参考MSDN吧。
然后有个比较重要的内容:我们需要模拟获得的令牌,因为OpenProcessToken获得的是进程的初始Token,不能直接用于访问权限的判断,我们要调用DuplicateToken()以当前用户的身份模拟一个同样的Token出来,具体使用待会儿看代码吧。
下面到了会让人比较困惑的地方:就是GENERIC_MAPPING这个结构体,这个开始看MSDN一直一头雾水,没理解到底怎么使用,MSDN上也没有代码实例。鼓捣了一上午最后发现其实很简单。比如我们使用CreateFile()创建一个文件的时候可以指定一些权限访问的标志如GENERIC_WRITE,GENERIC_READ等等。但是这些权限标志都是通用的标志,还可以用这些标志来创建或打开其他类型的对象。在表示文件对象的时候,这些通用标志所包含的实际文件对象特有的权限标志列表如下:
比如当我们想使用AccessCheck()检查当前进程对某文件是否有某种权限的时候,我们必须要调用MapGenericMask()把GENERIC_READ,GENERIC_WRITE,
GENERIC_EXECUTE等等这类通用权限控制标志映射成该类型的对象特有的权限控制标志,对于文件就是FILE_GENERIC_READ等。而这个函数中就用到了GENERIC_MAPPING这个结构体。最后就是调用AccessCheck(),参数还是比较复杂的,我这里简单介绍下,函数原型如下:
BOOL WINAPI AccessCheck(
__in PSECURITY_DESCRIPTOR pSecurityDescriptor,
__in HANDLE ClientToken,
__in DWORD DesiredAccess,
__in PGENERIC_MAPPING GenericMapping,
__out_opt PPRIVILEGE_SET PrivilegeSet,
__in_out LPDWORD PrivilegeSetLength,
__out LPDWORD GrantedAccess,
__out LPBOOL AccessStatus
);
pSecurityDescriptor是安全描述符表的指针没啥说的,ClientToken是模拟之后的令牌句柄。DesiredAccess是通用的权限控制标志。GenericMapping就是用MapGenericMask()映射后的针对特定对象的权限控制标志。 PrivilegeSet是我们之前提到过的访问令牌中的Privilege,用来检查一些系统操作的权限,比如开关机,修改系统时间等等,一般情况下初始化为0。PrivilegeSetLength是跟着之前PrivilegeSet的,这里既然不去检查权限也置为0。最后GrantedAccess和AccessStatus比较有用,AccessStatus会返回指定的权限是否被允许访问该对象,允许则为TRUE,否则为FALSE。如果AccessStatus为TRUE,该函数会把当前的ACE中的所有允许的权限操作标志赋给GrantedAccess。
下面给出获取令牌到检查权限部分的代码:
HANDLE hToken;
if(!OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken))
{
return bRs;
}
HANDLE hImpersonatedToken = NULL;
if(DuplicateToken(hToken,
SecurityImpersonation,&hImpersonatedToken))
//模拟令牌
{
DWORD dwGenericAccessMask = GENERIC_READ|GENERIC_WRITE;
GENERIC_MAPPING genMap ;
PRIVILEGE_SET privileges = {0};
DWORD grantAccess = 0;
DWORD privLength = sizeof(privileges);
BOOL bGrantAccess = FALSE;
//将通用权限控制标志和特定类型对象权限控制标志挂钩
genMap.GenericRead = FILE_GENERIC_READ;
genMap.GenericWrite = FILE_GENERIC_WRITE;
genMap.GenericExecute = FILE_GENERIC_EXECUTE;
genMap.GenericAll = FILE_ALL_ACCESS;
MapGenericMask(&dwGenericAccessMask,&genMap);
//映射通用权限控制标志
if(AccessCheck(psd,hImpersonatedToken,
dwGenericAccessMask, &genMap,&privileges,&privLength,&grantAccess,&bGrantAccess))
{
bRs = bGrantAccess;
return bRs;
}else
{
printf("\n[-]Access check failed:%d",GetLastError());
return bRs;
}
}
最后上图上真相吧:
文章到此结束了,拙作一篇,侧重于C+API编程实现对访问控制列表的遍历和权限的判断。只希望能让以后进行相关编程的同学能图个方便。Any comment is welcomed。
------------------------------------------------------------------------------------------------------------------------------------
可能有人会问,检测对文件或者文件夹的访问权限有什么用呢?哈哈,当然是有用的。假定目标程序是以标准用户权限运行的,QQ就是以标准用户权限运行的,不是一启动就申请管理员权限的,如果是一启动就要申请管理员权限,则在系统UAC打开时,每次启动都会弹出UAC提示框。并且在标准用户登录下,不使用管理员账户提权,是没法运行程序的。比如在实现IM中的文件传输功能时,文件接收端在另存文件时,是不能保存到一些敏感的系统目录C:\Windows、C:\Windows\System32,如果选择存到这些目录中,会被重定向到系统对应的虚拟路径中,导致用户找不到文件的。文件重定向的具体细节,可以参看:http://blog.csdn.net/chenlycly/article/details/53408212
最后附上本人最终用到实际工程中的完整代码 by chenlycly(xingpacer):(代码已经在win7、win8和win10中验证通过,要注意考虑的周全,该释放的资源的地方要记得释放,比如CloseHandle)
// 将要检测的权限GENERIC_XXXXXX传递给dwGenericAccessMask,可检测对
// 文件或者文件夹的权限
BOOL CanAccessFile( CString strPath, DWORD dwGenericAccessMask )
{
DWORD dwSize = 0;
PSECURITY_DESCRIPTOR psd = NULL;
SECURITY_INFORMATION si = OWNER_SECURITY_INFORMATION |
GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
// 获取文件权限信息结构体大小
BOOL bRet = GetFileSecurity( strPath, si, psd, 0, &dwSize );
if ( bRet || GetLastError() != ERROR_INSUFFICIENT_BUFFER )
{
return FALSE;
}
char* pBuf = new char[dwSize]; // 如果考虑频繁申请内存会失败,可考虑使用VirtualAlloc
ZeroMemory( pBuf, dwSize );
psd = (PSECURITY_DESCRIPTOR)pBuf;
// 获取文件权限信息结构体大小
bRet = GetFileSecurity( strPath, si, psd, dwSize, &dwSize );
if ( !bRet )
{
delete []pBuf;
return FALSE;
}
HANDLE hToken = NULL;
if ( !OpenProcessToken( GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken ) )
{
delete []pBuf;
return FALSE;
}
// 模拟令牌
HANDLE hImpersonatedToken = NULL;
if( !DuplicateToken( hToken, SecurityImpersonation, &hImpersonatedToken ) )
{
delete []pBuf;
CloseHandle( hToken );
return FALSE;
}
// 在检测是否有某个权限时,将GENERIC_WRITE等值传给本函数的第二个参数dwGenericAccessMask
// GENERIC_WRITE等参数在调用CreateFile创建文件时会使用到,下面调用MapGenericMask将
// GENERIC_WRITE等转换成FILE_GENERIC_WRITE等
// 将GENERIC_XXXXXX转换成FILE_GENERIC_XXXXXX
GENERIC_MAPPING genMap;
genMap.GenericRead = FILE_GENERIC_READ;
genMap.GenericWrite = FILE_GENERIC_WRITE;
genMap.GenericExecute = FILE_GENERIC_EXECUTE;
genMap.GenericAll = FILE_ALL_ACCESS;
MapGenericMask( &dwGenericAccessMask, &genMap );
// 调用AccessCheck来检测是否有指定的权限
PRIVILEGE_SET privileges = { 0 };
DWORD dwGrantedAccess = 0;
DWORD privLength = sizeof(privileges);
BOOL bGrantedAccess = FALSE;
if( AccessCheck( psd, hImpersonatedToken, dwGenericAccessMask,
&genMap, &privileges, &privLength, &dwGrantedAccess, &bGrantedAccess ) )
{
}
else
{
bGrantedAccess = FALSE;
}
delete []pBuf;
CloseHandle( hImpersonatedToken );
CloseHandle( hToken );
return bGrantedAccess;
}
经产品测试人员测试发现,上述代码在win7中没问题,但是在win10中判断是失效的,比如对于一些敏感的系统目录C:\Windows、C:\Windows\System32,如果目标程序没有管理员权限(以标准用户权限运行),肯定是没有写权限的!但是上述代码却判断不出来,后来通过添加AccessCheck调用后的参数信息的打印,找到了解决办法,需要对上述代码做部分改动!