閱讀本文的朋友需要對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來進行訪問權限的檢查。本文主要針對文件對象進行介紹,其他類型的對象大同小異。要用到的兩個主要API為GetFileSecurity()和AccessCheck()。GetFileSecurity能夠獲取指定文件的安全描述符表,而AccessCheck可以指定要檢查的權限,函數能夠將獲得的安全描述符表與當前進程的Token進行檢查來判斷進程對該文件對象是否允許相應的權限。不過這兩個API並不那麼容易用,因為其中要涉及到安全描述符表和訪問令牌的獲取,因此又牽扯出一大把API也涉及一些訪問控制的知識。下面依次介紹要使用到的API然後給出整體的代碼。GetFileSecurity的函數原型如下:
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,
FILE_GENERIC_WRITE等等。
最後就是調用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。
本文出自 “About:Blank H4cking” 博客,請務必保留此出處http://pnig0s1992.blog.51cto.com/393390/908495