程式設計實現遍歷ACL訪問控制列表檢查程式訪問許可權

技術小美發表於2017-11-22

首發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(
    [-]Get SD failed:%d”
    ,GetLastError()); 
  21.                             return bRs; 
  22.                    } 
  23.          }         if(!GetFileSecurity(lpFileName,si,psd,dwSizeNeeded,&dwSizeNeeded)) 
  24.          { 
  25.                    printf(
    [-]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(
    [-]Access check failed:%d”
    ,GetLastError()); 
  35.                            return bRs; 
  36.                   } 
  37.          } 

最後上圖上真相吧:

文章到此結束了,拙作一篇,側重於C+API程式設計實現對訪問控制列表的遍歷和許可權的判斷。只希望能讓以後進行相關程式設計的同學能圖個方便。Any comment is welcomed















本文轉hackfreer51CTO部落格,原文連結:http://blog.51cto.com/pnig0s1992/908495,如需轉載請自行聯絡原作者


相關文章