编程实现遍历ACL访问控制列表检查进程访问权限

本文涉及的产品
访问控制,不限时长
云防火墙,500元 1000GB
简介:

首发Freebuf.com了,欢迎关注FreeBuf.

Author:Pnig0s[FreeBuf]

阅读本文的朋友需要对Windows访问控制模型有初步的了解,了解Token(访问令牌)ACL(访问控制列表)DACL(选择访问控制列表)ACE(访问控制列表项)等与访问控制模型相关的名词含义及之间的关系,当然我也会在文中简要科普一下ACM 

写这篇文章的目的主要是最近在写一个Win下本地提权的东西,涉及到了对ACL的操作,以前对ACL总是避而远之,Windows访问控制模型很复杂很头疼一个API会牵出一大把初始化要用的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。整体的一个权限检查过程如下图:

 --->>

上面简单介绍了本文要用到的也是Windows访问控制模型核心部分的一些知识,下面来介绍下如何编程实现遍历ACL来进行访问权限的检查。本文主要针对文件对象进行介绍,其他类型的对象大同小异。要用到的两个主要APIGetFileSecurity()AccessCheck()GetFileSecurity能够获取指定文件的安全描述符表,而AccessCheck可以指定要检查的权限,函数能够将获得的安全描述符表与当前进程的Token进行检查来判断进程对该文件对象是否允许相应的权限。不过这两个API并不那么容易用,因为其中要涉及到安全描述符表和访问令牌的获取,因此又牵扯出一大把API也涉及一些访问控制的知识。下面依次介绍要使用到的API然后给出整体的代码。GetFileSecurity的函数原型如下:


 
 
  1. BOOL WINAPI GetFileSecurity(  
  2.   __in          LPCTSTR lpFileName,  
  3.   __in          SECURITY_INFORMATION RequestedInformation,  
  4.   __out_opt     PSECURITY_DESCRIPTOR pSecurityDescriptor,  
  5.   __in          DWORD nLength,  
  6.   __out         LPDWORD lpnLengthNeeded  
  7. );  

lpFileName指定了要获取SD的文件。首先要定义一个PSECURITY_DESCRIPTOR的安全描述符表指针,因为描述符表大小未知,所以要调用两次GetFileSecurity()第一次将nLength0,函数会返回实际大小,然后第二次用获取的大小去接收完整的SD,代码如下:

文件开始部分定义的内存分配释放函数常量:


 
 
  1. #define AllocMem(x) (HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,x)) 
  2. #define FreeMem(x) (HeapFree(GetProcessHeap(),HEAP_ZERO_MEMORY,x)) 
  3. ... 
  4. ... 
  5. BOOL bRs = FALSE; 
  6. DWORD dwSizeNeeded = 0; 
  7. PSECURITY_DESCRIPTOR psd = NULL; 
  8. SECURITY_INFORMATION si = OWNER_SECURITY_INFORMATION 
  9. | GROUP_SECURITY_INFORMATION|DACL_SECURITY_INFORMATION;         
  10.          bRs = GetFileSecurity(lpFileName,si,psd,0,&dwSizeNeeded); 
  11.          //第一次调用获得SD实际大小 
  12.          if(!bRs) 
  13.          { 
  14.                    if(GetLastError() == ERROR_INSUFFICIENT_BUFFER) 
  15.                    { 
  16.                             psd = (PSECURITY_DESCRIPTOR)AllocMem(dwSizeNeeded); 
  17.                             //根据获取到的大小对psd分配内存 
  18.                    }else 
  19.                    { 
  20.                             printf("\n[-]Get SD failed:%d",GetLastError()); 
  21.                             return bRs; 
  22.                    } 
  23.          }         if(!GetFileSecurity(lpFileName,si,psd,dwSizeNeeded,&dwSizeNeeded)) 
  24.          { 
  25.                    printf("\n[-]Get SD failed:%d",GetLastError()); 
  26.                    return bRs; 
  27.          } 

至此针对指定文件对象的安全描述符表已经得到,下一步需要提取出访问进程的访问令牌(Token)。首先调用OpenProcessToken()获得本进程的Token,参数比较简单参考MSDN吧。然后有个比较重要的内容,我们需要模拟获得的令牌,因为OpenProcessToken获得的是进程的初始Token,不能直接用于访问权限的判断,我们要调用DuplicateToken()以当前用户的身份模拟一个同样的Token出来,具体使用待会儿看代码吧。下面到了最坑爹的一部分,就是GENERIC_MAPPING这个结构体,这个开始看MSDN一直一头雾水,没理解到底怎么使用,MSDN上也没有代码实例。鼓捣了一上午最后发现其实很简单,只是没有清晰描述没资料有点儿困惑。比如我们使用CreateFile()创建一个文件的时候可以指定一些权限访问的标志如GENERIC_WRITEGENERIC_READ等等。但是这些权限标志都是通用的标志,还可以用这些标志来创建或打开其他类型的对象。在表示文件对象的时候,这些通用标志所包含的实际文件对象特有的权限标志列表如下:

比如当我们想使用AccessCheck()检查当前进程对某文件是否有读权限的时候,我们必须要调用MapGenericMask()GENERIC_READ,GENERIC_WRITE,GENERIC_EXECUTE等等这类通用权限控制标志映射成该类型的对象特有的权限控制标志,对于文件就是FILE_GENERIC_READ,

FILE_GENERIC_WRITE等等。

最后就是调用AccessCheck(),参数还是比较复杂的,我这里简单介绍下,函数原型如下:


 
 
  1. BOOL WINAPI AccessCheck(  
  2.   __in          PSECURITY_DESCRIPTOR pSecurityDescriptor,  
  3.   __in          HANDLE ClientToken,  
  4.   __in          DWORD DesiredAccess,  
  5.   __in          PGENERIC_MAPPING GenericMapping,  
  6.   __out_opt     PPRIVILEGE_SET PrivilegeSet,  
  7.   __in_out      LPDWORD PrivilegeSetLength,  
  8.   __out         LPDWORD GrantedAccess,  
  9.   __out         LPBOOL AccessStatus  
  10. );  

pSecurityDescriptor是安全描述符表的指针没啥说的,ClientToken是模拟之后的令牌句柄。DesiredAccess是通用的权限控制标志。GenericMapping就是用MapGenericMask()映射后的针对特定对象的权限控制标志。 PrivilegeSet是我们之前提到过的访问令牌中的Privilege,用来检查一些系统操作的权限,比如开关机,修改系统时间等等,一般情况下初始化为0PrivilegeSetLength是跟着之前PrivilegeSet的,这里既然不去检查权限也置为0。最后GrantedAccessAccessStatus比较有用,AccessStatus会返回指定的权限是否被允许访问该对象,允许则为TRUE,否则为FALSE。如果AccessStatusTRUE,该函数会把当前的ACE中的所有允许的权限操作标志赋给GrantedAccess

下面给出获取令牌到检查权限部分的代码:


 
 
  1. HANDLE hToken;        
  2. if(!OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken)) 
  3.          { 
  4.                    return bRs; 
  5.          } 
  6.  
  7.          HANDLE hImpersonatedToken = NULL; 
  8.          if(DuplicateToken(hToken, 
  9.                    SecurityImpersonation,&hImpersonatedToken)) 
  10.          //模拟令牌 
  11.  
  12.          { 
  13.                    DWORD dwGenericAccessMask = GENERIC_READ|GENERIC_WRITE; 
  14.                    GENERIC_MAPPING genMap ; 
  15.                    PRIVILEGE_SET privileges = {0}; 
  16.                    DWORD grantAccess = 0; 
  17.                    DWORD privLength = sizeof(privileges); 
  18.                    BOOL bGrantAccess = FALSE; 
  19.                    //将通用权限控制标志和特定类型对象权限控制标志挂钩 
  20.                    genMap.GenericRead = FILE_GENERIC_READ; 
  21.                    genMap.GenericWrite = FILE_GENERIC_WRITE; 
  22.                    genMap.GenericExecute = FILE_GENERIC_EXECUTE; 
  23.                    genMap.GenericAll = FILE_ALL_ACCESS; 
  24.  
  25.                    MapGenericMask(&dwGenericAccessMask,&genMap); 
  26.                    //映射通用权限控制标志 
  27.                    if(AccessCheck(psd,hImpersonatedToken, 
  28.                             dwGenericAccessMask,                            &genMap,&privileges,&privLength,&grantAccess,&bGrantAccess)) 
  29.                    { 
  30.                             bRs = bGrantAccess; 
  31.                             return bRs; 
  32.                    }else 
  33.                    { 
  34.                             printf("\n[-]Access check failed:%d",GetLastError()); 
  35.                            return bRs; 
  36.                   } 
  37.          } 

最后上图上真相吧:

文章到此结束了,拙作一篇,侧重于C+API编程实现对访问控制列表的遍历和权限的判断。只希望能让以后进行相关编程的同学能图个方便。Any comment is welcomed















本文转hackfreer51CTO博客,原文链接:http://blog.51cto.com/pnig0s1992/908495,如需转载请自行联系原作者

相关文章
|
5月前
|
消息中间件 存储 缓存
【嵌入式软件工程师面经】Linux系统编程(线程进程)
【嵌入式软件工程师面经】Linux系统编程(线程进程)
125 1
|
6天前
|
安全 网络安全 数据安全/隐私保护
访问控制列表(ACL)是网络安全中的一种重要机制,用于定义和管理对网络资源的访问权限
访问控制列表(ACL)是网络安全中的一种重要机制,用于定义和管理对网络资源的访问权限。它通过设置一系列规则,控制谁可以访问特定资源、在什么条件下访问以及可以执行哪些操作。ACL 可以应用于路由器、防火墙等设备,分为标准、扩展、基于时间和基于用户等多种类型,广泛用于企业网络和互联网中,以增强安全性和精细管理。
37 7
|
4月前
|
安全 Python
告别低效编程!Python线程与进程并发技术详解,让你的代码飞起来!
【7月更文挑战第9天】Python并发编程提升效率:**理解并发与并行,线程借助`threading`模块处理IO密集型任务,受限于GIL;进程用`multiprocessing`实现并行,绕过GIL限制。示例展示线程和进程创建及同步。选择合适模型,注意线程安全,利用多核,优化性能,实现高效并发编程。
74 3
|
2月前
|
存储 算法 Linux
C语言 多进程编程(一)进程创建
本文详细介绍了Linux系统中的进程管理。首先,文章解释了进程的概念及其特点,强调了进程作为操作系统中独立可调度实体的重要性。文章还深入讲解了Linux下的进程管理,包括如何获取进程ID、进程地址空间、虚拟地址与物理地址的区别,以及进程状态管理和优先级设置等内容。此外,还介绍了常用进程管理命令如`ps`、`top`、`pstree`和`kill`的使用方法。最后,文章讨论了进程的创建、退出和等待机制,并展示了如何通过`fork()`、`exec`家族函数以及`wait()`和`waitpid()`函数来管理和控制进程。此外,还介绍了守护进程的创建方法。
C语言 多进程编程(一)进程创建
|
2月前
|
安全 开发者 Python
揭秘Python IPC:进程间的秘密对话,让你的系统编程更上一层楼
【9月更文挑战第8天】在系统编程中,进程间通信(IPC)是实现多进程协作的关键技术。IPC机制如管道、队列、共享内存和套接字,使进程能在独立内存空间中共享信息,提升系统并发性和灵活性。Python提供了丰富的IPC工具,如`multiprocessing.Pipe()`和`multiprocessing.Queue()`,简化了进程间通信的实现。本文将从理论到实践,详细介绍各种IPC机制的特点和应用场景,帮助开发者构建高效、可靠的多进程应用。掌握Python IPC,让系统编程更加得心应手。
32 4
|
2月前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
2月前
|
Linux C语言
C语言 多进程编程(四)定时器信号和子进程退出信号
本文详细介绍了Linux系统中的定时器信号及其相关函数。首先,文章解释了`SIGALRM`信号的作用及应用场景,包括计时器、超时重试和定时任务等。接着介绍了`alarm()`函数,展示了如何设置定时器以及其局限性。随后探讨了`setitimer()`函数,比较了它与`alarm()`的不同之处,包括定时器类型、精度和支持的定时器数量等方面。最后,文章讲解了子进程退出时如何利用`SIGCHLD`信号,提供了示例代码展示如何处理子进程退出信号,避免僵尸进程问题。
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
2月前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。

相关实验场景

更多