本文旨在幫助讀者對 discretionary access control lists (DACLs) 及其 access control entries (ACEs) 有中間程度的理解。讀者應牢固掌握 C++,並對 Microsoft 的 Windows 平臺及其 WinAPI 有一定經驗,如果希望測試此程式碼,還需要能夠訪問執行 Windows 的系統。此程式碼在任何 POSIX 平臺上都無法正常工作。請參閱頁面底部的原始碼連結。
DACL 是檔案或資料夾屬性頁“安全”選項卡下的許可權列表。它們用於管理使用者或組對特定檔案或目錄的訪問許可權。沒有 DACL 的檔案允許 Everyone 完全訪問,請注意,Everyone 是 Microsoft Windows 平臺中的一個安全組,旨在包含所有使用者、組和服務。如果存在 DACL,則僅允許它明確列出的使用者或組的訪問許可權。沒有列出許可權的使用者或組將被拒絕訪問(檔案或資料夾的所有者使用者或組是例外),這意味著一個空白 DACL 的檔案與沒有 DACL 的檔案不同,因為空白 DACL 會拒絕所有使用者和組的任何訪問。
以下程式碼將在可執行檔案執行的當前工作目錄中建立一個檔案(名為“New.txt”),此目錄中同名同副檔名的檔案將被刪除。首次建立時,檔案將具有系統授予的預設安全許可權以及從其父目錄繼承的任何許可權。鼓勵讀者在第 111 行給出的暫停期間檢查這些許可權,並將輸出(第 108 行)與其中列出的內容進行比較。程式碼中大部分使用的函式和結構都提供了指向相應 MSDN 條目的註釋。此專案應連結 AdvApi32.lib 和 User32.lib(對於 MSVS 使用者)或 libadvapi32.a 和 libuser32.a(對於 Mingw 使用者)。
我們的程式碼執行的第一個實際工作是在第 27 行宣告一個指向 SECURITY_DESCRIPTOR 結構的指標。該結構使用“new”運算子在記憶體中分配空間,並使用“InitializeSecurityDescriptor()”函式進行初始化。此函式將 SECURITY_DESCRIPTOR 結構的所有成員(安全修訂級別除外)設定為空白。此時,SECUIRTY_DESCRIPTOR_REVISION 是此函式的“dwRevision”的唯一有效值。
接下來要做的就是根據可執行檔案的當前工作目錄計算要操作的檔名。這是透過“GetCurrentDirectory()”函式獲取當前目錄名,然後使用“sprinf_s()”函式在其末尾追加“\New.txt”來完成的,這樣做只是為了讓我們在執行此程式碼時少操心一件事。
接下來是宣告和初始化我們的 EXPLICIT_ACCESS 結構陣列。首先在此說明一點,本節以冗長的方式編寫,以確保讀者瞭解我們每一步在做什麼,有一些事情可以節省您大量打字(如果您喜歡的話),但本文的重點是可讀性而不是作者的便利性。您可以將它們視為實際的 ACE,然後由系統處理為可用格式,它們處於所謂的“自相對格式”中。為了使它們可供系統使用,拒絕任何使用者或組訪問的條目必須列在授予訪問許可權的條目之前。我們使用的“SetEntriesInAcl()”函式會自動為新條目新增到現有條目中,但不會為現有條目新增到新條目中。這不是“按使用者”基礎,而是所有許可權的總體排序,因此:UserA:拒絕許可權,UserA:授予許可權,UserB:拒絕許可權,UserB:授予許可權;這樣的模式是無效的。我們的第一個條目 (ExplicitAccess[0]) 被清零,然後使用“BuildExplicitAccessWithName()”函式進行初始化。此函式有效地與我為其他兩個條目使用的初始化結構例項相同,就我們的目的而言,這兩種方法提供的控制程度並沒有更多或更少。我們首先透過將 ExplicitAccess[0] 按引用作為第一個引數傳遞來告訴函式我們的 EXPLICIT_ACCESS 結構儲存在記憶體中的位置。然後我們告訴函式我們要為哪個帳戶設定此條目,我選擇了“Guest”帳戶,因為它適用於所有版本的 Windows 作業系統,即使它被停用(自 Windows XP 開始預設停用),此設定仍然有效。第三個引數是我們希望處理的許可權列表,每個許可權都代表一個數字,我們可以使用按位 OR 運算子將它們組合成一個引數後再傳遞給函式。第四個引數告訴函式要設定哪種型別的 ACE,我們選擇 DENY_ACCESS,因為這就是我們想要做的,並且請記住,所有拒絕訪問的條目都必須先輸入。最後一個引數有該函式文件中列出的多種可能性,我選擇 NO_PROPAGATE_INHERIT_ACE 來稍後展示如何透過僅更改檔名並將“CreateFile()”替換為“CreateDirectory()”來將這些相同的方法應用於設定資料夾許可權。
我們的 ExplicitAccess 陣列中的另外兩個條目是透過手動定義的;在這樣做時,必須記住 WinAPI 是為 C 編寫的,因此沒有設定預設值的建構函式,未輸入的任何內容在技術上都是未定義的。ExplicitAccess[1] 條目授予“Authenticated Users”組所有訪問許可權(GENERIC_ALL),這是一個 Windows 預設內建的組帳戶,應該可以在您的系統上正常工作。這裡唯一值得注意的條目是 Trustee 成員 pMultipleTrustee、MultipleTrusteeOperation 和 TrusteeType。在撰寫本文時,前兩個列表不受支援,必須設定為我在這裡的值。第三個設定為 TRUSTEE_IS_GROUP,以告知系統您在此條目中設定許可權的帳戶是一個組,而不是使用者或系統。如果您還沒有猜到,Trustee 成員 pstrName 是您希望為其建立條目的使用者或組的名稱。ExplicitAccess[2] 設定為另一個與“Guest”帳戶相關的條目。我這樣做的原因有兩個:首先是展示定義 EXPLICIT_ACCESS 結構的兩種方法是可互換的,其次是展示條目所涉及的帳戶可以按任何順序進行,只要所有拒絕任何帳戶許可權的條目都放在前面。
現在我們進入 try catch 塊,用於在發生錯誤時確保程式碼的正確清理。第 80 行的“DeleteFile()”命令是為了防止您多次執行此過程,並且您不想每次都刪除它建立的檔案。我注意到,即使在“CreateFile()”中設定了 CREATE_ALWAYS 標誌,ACE 在檔案被覆蓋時仍然保留,因此為了看到所做的更改,必須每次刪除目標檔案。如果檔案不存在,“DeleteFile()”將返回 FALSE,但我們在程式繼續執行時會忽略該值。
首先,我們呼叫“CreateFile()”來實際建立我們將要操作的檔案。這裡唯一真正值得注意的是我們傳遞給 lpSecurityAttributes 的引數是 NULL。我們這樣做是為了讓系統使用您系統的預設安全許可權來建立我們的檔案,這些許可權應該只包括執行程式的使用者的許可權(您)以及可能從父目錄繼承的任何安全許可權。如果成功,此函式將建立我們的檔案並返回一個控制代碼,我們從此處開始使用該控制代碼來引用檔案本身;否則,將丟擲並捕獲一個錯誤,我們的程式會輸出一個錯誤程式碼,您可以在此處查詢:http://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx
接下來,在第 91 行,我們呼叫“GetNamedSecurityInfo()”函式,正如您可能猜到的,該函式允許您獲取命名物件的安全資訊。在這種情況下,我們使用剛剛建立的檔名,這不成問題,因為我們在建立檔案時將 lpSecurityAttributes 標誌設定為 NULL,因此您將擁有執行此操作所需的訪問許可權。對於 lpSecuirytAttributes 的任何其他引數或預先存在的檔案,在此函式成功執行之前可能需要進行一些額外的工作。此函式“GetNamedSecurityInfo()”實際上可以透過組合第三個引數的值和按位 OR 運算子來返回物件的任意數量的屬性(假設它們存在),但今天我們只關注此檔案“New.txt”的 DACL。作為第一個引數,我們傳遞檔名。然後第二,我們告訴它我們的物件是 SE_FILE_OBJECT 型別。第三個是 SECURITY_INFORMATION 標誌(DWORD),指示我們正在查詢的內容,在這種情況下,我們想要我們第一個引數命名的物件的 DACL。接下來的兩個引數 ppsidGroup 和 ppsidOwner 可以是指標,它們包含指向物件所有者和/或主要組的 SID 的指標(同樣,假設它們存在),但今天我們不關注這些,所以我們將它們設定為 NULL。現在,到第六個引數,我們來到了 ppDacl,這是我們傳遞以接收指向物件當前 discretionary access control list 的指標的引數。此處至關重要的是要注意此函式此引數的定義是指向指標的指標,因此您不能傳遞 ACL 的實際例項或對 ACL 的引用。此引數必須是指向 ACL 的指標,並透過引用傳遞,使用“&”運算子。下一個變數 ppSacl 可以是指向正在查詢的物件的 SACL 的指標,但此 ACL 可能不存在,並且不是本文的重點,因此我們將其傳遞為 NULL。最後是指向將儲存我們命名的物件的 DACL 的指標。為此,我們使用對變數“pSecurityDescriptor”的引用,該變數在第 27 行定義為指向 SECURITY_DESCRIPTOR 資料型別的指標。此引數將接收此函式請求的實際安全描述符或安全描述符的組合。
“GetNamedSecurityInfo()”返回的安全描述符不是您能理解的格式,因此為了真正從中收集有意義的資訊,我們在第 100 行使用“ConvertSecurityDescriptorToStringSecurityDescriptor()”函式處理字串。此函式首先需要一個指向我們安全資訊結構的指標,即由“GetNamedSecurityInfo()”函式填充的“pSecurityDescriptor”。接下來,您需要指定您傳入的安全描述符的修訂號,在本文撰寫時,僅支援 SDDL_REVISION_1。第三個引數與傳遞給“GetNamedSecurityInfo()”的標誌組合相同。第四個引數是指向實際將儲存安全描述符資訊的純文字字串的指標。我們使用在第 41 行定義的 DACLDescriptorAsString 變數作為指向新 LPSTR 的 LPSTR 指標。這可能看起來令人困惑,但 LPSTR 是指向 CHAR 資料型別的指標,因此 DACLDescriptorAsString 實際上是指向指向 CHAR 型別的指標。第五個也是最後一個引數是安全描述符字串的長度,您可以將其設為 NULL,但我選擇將其儲存在 SecDescStringLengthNeeded 中,沒有特別的原因。現在要輸出此字串,我們只需使用 std::cout 並將 DACLDescriptorAsString 解引用一次,就得到了一個指向 CHAR 陣列的指標,該陣列包含我們要顯示的資料。
在這裡,我使用了我的“pause()”函式來使程式暫停,您可以隨意使用偵錯程式,但我不想排除那些不知道如何使用它的使用者。程式暫停是為了讓您可以將儲存在 DACLDescriptorAsString 中的字串與 Windows 資源管理器中此檔案屬性的“安全”選項卡下顯示的內容進行比較。要繼續處理,只需按 Enter 鍵即可,但讓我們花點時間看看控制檯視窗中顯示的字串,因為您會注意到這並非完全是純文字。安全描述符字串有自己的格式,在程式碼的第 105 行的 URL 中有詳細描述。我的字串以“D:“開頭,這表示該字串是 DACL。接下來的部分是第一個 ACE 字串(如果您想查詢您字串中條目的特定含義,可以在第 106 行找到連結),它以“(A;”開頭,這告訴我這個描述符的目的是定義一個 ACCESS_ALLOWED_ACE_TYPE,它允許訪問與其關聯的物件。我接下來看到“ID;FA;”,這些是標誌,告訴我是 INHERITED_ACE 型別,具有 FILE_ALL_ACCESS 許可權。object_gui 和 inherit_object_gui 的條目是“;;”,因為這是一個文字檔案,所以它沒有 GUI。最後一個條目是一個很長的字串,實際上是我的 Windows 帳戶的 SID,後跟一個關閉的大括號“)”,表示該訪問控制條目的結束。接下來的兩個 ACE 字串與第一個幾乎相同,只是每個字串的最後一個條目指示了字串所屬的帳戶。如果存在我這裡的這兩個,那麼您會注意到它們要短得多,每個只包含兩個字元:“BA”和“SY”。如果我們轉到第 107 行的 URL,我們會看到 SID 字串格式文件,其中告訴我們這些是 SDDL_BUILTIN_ADMINISTRATORS 和 SDDL_LOCAL_SYSTEM 帳戶的常量值。
此時,您可以按 Enter 鍵繼續程式的執行。如果一切順利,您的程式將在第 114 行繼續,在那裡我們呼叫“SetEntriesInAcl()”來將我們之前定義的 EXPLICIT_ACCESS 結構用於檔案訪問控制列表 (ACL) 的實際條目。對於第一個引數,我使用了與定義我們的 EXPLICIT_ACCESS 陣列大小相同的 const 表示式,因為這兩個值沒有理由不同。第二個引數需要一個指向我們想要用於定義新 ACL 的 EXPLICIT_ACCESS 結構的指標列表。由於我們的 EXPLICI_ACCESS 條目儲存在陣列中,我們只需傳遞其名稱。第三個條目是指向我們舊 DACL 的指標,這個是可選的,但既然我們有了,我們就傳遞它。如果我們不包含舊 DACL,則將使用我們提供的 EXPLICIT_ACCESS 結構和從父資料夾繼承的任何許可權建立全新的 DACL。要看到這一點,您需要註釋掉第 80 行的“DeleteFile()”命令,將第 114 行的‘pOldDacl’變數替換為‘NULL’,並在執行程式之前透過屬性->安全選項卡在 Windows 資源管理器中為檔案新增另一個使用者或訪問許可權。您將看到您透過 Windows 資源管理器手動新增的條目將被刪除。最後,“SetEntriesInAcl()”的最後一個引數是指標,該指標接收此函式建立的新 ACL 的指標。然後我們使用“IsValidAcl()”函式在第 123 行檢查我們新 ACL 的一致性。
最後我們來到第 129 行,這是“SetNamedSecurityInfo()”,這個函式幾乎是我們開始時使用的“GetNamedSecurityInfo()”函式的反向操作。它根據您在第三個引數中傳遞的標誌以及您之後傳遞的結構來設定物件的安全資訊。在這裡,您可以看到我們只告訴它設定 DACL_SECURITY_INFORMATION,並且我們只傳遞了剛剛建立的 pNewDacl。此函式設定資訊而不返回它,因此沒有等同於 ppSecurityDescriptor 的條目。
在那之後,您的檔案現在已設定了您提供的新安全描述符資訊。我們獲取當前的 DACL(現在是新的),然後像以前一樣將其轉換為字串,以便您可以將舊條目與新條目逐一進行比較。
附件:[main.cpp]