WindowsVistaforDevelopers——第四部分:使用者帳號控制(UserAccountControl,UAC)

maojunxu發表於2018-03-08

作者:Kenny Kerr

翻譯:Dflying Chen

原文:http://weblogs.asp.net/kennykerr/archive/2006/09/29/Windows-Vista-for-Developers-_1320_-Part-4-_1320_-User-Account-Control.aspx

請同時參考《Windows Vista for Developers》系列

 

自從Windows 2000以來,Windows開發者一直試圖為使用者創造一個安全穩妥的工作環境。Windows 2000引入了一種名為“受限訪問令牌(Restricted Token)”的技術,能夠有效地限制應用程式的許可和許可權。Windows XP則在安全方面更進一步,不過對於普通使用者來講,這種安全控制卻並不是那麼的深入人心……直到現在為止還是如此。不管你最初反對的理由是什麼,現在使用者帳號控制(User Account Control,UAC)就擺在你的面前,其實它並不像批評中所說的那樣一無是處。作為開發者的我們有責任掌握這項技術,進而讓我們所開發的Vista應用程式不會總是彈出那些“討厭”的提示視窗。

《Windows Vista for Developers》系列文章的第四部分中,我們將從實際出發探索一下UAC的功能,特別是如何以程式設計方式使用這些特性。

 

什麼是安全上下文(Security Context)?

安全上下文指的是一類定義某個程式允許做什麼的許可和許可權的集合。Windows中的安全上下文是通過登入會話(Logon Session)定義的,並通過訪問令牌維護。顧名思義,登入會話表示某個使用者在某臺計算機上的某次會話過程。開發者可以通過訪問令牌與登入會話進行互動。訪問令牌所有用的許可和許可權可以與登入會話的不同,但始終是它的一個子集。這就是UAC工作原理中的最核心部分。

 

那麼UAC的工作原理是什麼呢?

在Windows Vista作業系統中,有兩種最主要的使用者帳號:標準使用者(stand user)和管理員(administrator)。你在計算機上建立的第一個使用者將成為管理員,而後續使用者按照預設設定將成為標準使用者。標準使用者用來提供給那些不信任自己能夠控制整個計算機的使用者,而管理員則為那些希望能夠完全控制計算機的使用者所準備。與先前版本的Windows不同,在Windows Vista中,你不再需要以標準使用者的身份登入到系統中以便防止某些惡意程式碼/程式的惡意行為。標準使用者和管理員的登入會話擁有同樣的保護電腦保安的能力。

當一個標準使用者登入到計算機時,Vista將建立一個新的登入會話,並通過一個作業系統建立的、與剛剛建立的這個登入會話相關聯的shell程式(例如Windows Explorer )作為訪問令牌頒發給使用者。

而當一個管理員登入到計算機時,Windows Vista的處理方式卻與先前版本的Windows 有所不同。雖然系統建立了一個新的登入會話,但卻為該登入會話建立了兩個不同的訪問令牌,而不是先前版本中的一個。第一個訪問令牌提供了管理員所有的許可和許可權,而第二個就是所謂的“受限訪問令牌”,有時候也叫做“過濾訪問令牌(filtered token)”,該令牌提供了少得多的許可和許可權。實際上,受限訪問令牌所提供的訪問許可權和標準使用者的令牌沒什麼區別。然後系統將使用該受限訪問令牌建立shell應用程式。這也就意味著即使使用者是以管理員身份登入的,其預設的執行程式許可和許可權仍為標準使用者。

若是該管理員需要執行某些需要額外許可和許可權的、並不在受限訪問令牌提供許可權之內的操作,那麼他/她可以選擇使用非限制訪問令牌所提供的安全上下文來執行該應用程式。在由受限訪問令牌“提升”到非限制訪問令牌的過程中,Windows Vista將通過給管理員提示的方式確認該操作,以其確保計算機系統的安全。惡意程式碼不可能繞過該安全提示並在使用者不知不覺中得到對計算機的完整控制。

正如我前面提到的那樣,受限訪問令牌並不是Windows Vista中的新特性,但在Windows Vista中,該特性終於被無縫地整合到使用者的點滴操作中,並能夠實實在在地保護使用者在工作(或遊戲)時的安全。

 

受限訪問令牌

雖然在通常情況下,我們不用自行建立受限訪問令牌,但瞭解其建立的過程卻非常有用,因為它可以幫助我們更好地理解受限訪問令牌能夠為我們做什麼,進而更深入地瞭解我們的程式將執行於的環境。作為開發者,我們可能需要建立一個比UAC提供的更為嚴格的約束環境,這時瞭解如何建立受限訪問令牌就顯得至關重要了。

這個名副其實的CreateRestrictedToken 函式用來根據現有的訪問令牌的約束建立一個新的訪問令牌。該令牌可以用如下的方式約束訪問許可權:

  1. 通過指定禁用安全標示符(deny-only security identifier,deny-only SID)限制訪問需要被保護的資源。
  2. 通過指定受限SID實現額外的訪問檢查。
  3. 通過刪除許可權。

UAC所使用的受限訪問令牌在建立時指定了禁用SID並刪除了某些許可權,而並沒有使用受限SID。讓我們通過一個簡單示例說明這一點。第一步就是得到當前正在使用的訪問令牌,以便稍後進行復制並基於它刪除某些許可權:

CHandle processToken;
VERIFY(::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY,
                          &processToken.m_h));

接下來需要搞定需要禁用的SID陣列,以確保這些SID不能被用來訪問資源。下面的程式碼使用了我編寫的WellKnownSid 類建立系統內建的管理員組的SID。WellKnownSid 類可以在本文的下載程式碼中找到。

WellKnownSid administratorsSid = WellKnownSid::Administrators();
SID_AND_ATTRIBUTES sidsToDisable[] = 
{
    &administratorsSid, 0
    // add additional SIDs to disable here
};
Next we need an array of privileges to delete. We first need to look up the privilege’s LUID value:
LUID shutdownPrivilege = { 0 };
VERIFY(::LookupPrivilegeValue(0, // local system
                              SE_SHUTDOWN_NAME,
                              &shutdownPrivilege));
LUID_AND_ATTRIBUTES privilegesToDelete[] = 
{
    shutdownPrivilege, 0
    // add additional privileges to delete here
};

然後即可呼叫CreateRestrictedToken 函式建立該受限訪問令牌:

CHandle restrictedToken;
VERIFY(::CreateRestrictedToken(processToken,
                               0, // flags
                               _countof(sidsToDisable),
                               sidsToDisable,
                               _countof(privilegesToDelete),
                               privilegesToDelete,
                               0, // number of SIDs to restrict,
                               0, // no SIDs to restrict,
                               &restrictedToken.m_h));

這樣,我們指定了SE_GROUP_USE_FOR_DENY_ONLY 標記所得到的訪問令牌就將包含內建的管理員組的SID,不過這些SID是用來禁用,而不是用來允許訪問的。我們還剝奪了該訪問令牌的SeShutdownPrivilege 許可權,保證該令牌不能重新啟動、休眠或關閉計算機。

若你覺得很有意思,那麼可以嘗試做個小實驗。將上面的程式碼拷貝到某個控制檯應用程式中,然後新增如下的CreateProcessAsUser 函式呼叫,並相應地更新程式碼中Windows Explorer可執行程式的路徑:

STARTUPINFO startupInfo = { sizeof (STARTUPINFO) };
ProcessInfo processInfo;
VERIFY(::CreateProcessAsUser(restrictedToken,
                             L"C:\Windows\Explorer.exe",
                             0, // cmd line
                             0, // process attributes
                             0, // thread attributes
                             FALSE, // don`t inherit handles
                             0, // flags
                             0, // inherit environment
                             0, // inherit current directory
                             &startupInfo,
                             &processInfo));

現在殺掉計算機中的所有Explorer.exe 程式並執行上述程式碼。你會注意到再也無法重新啟動、休眠或關閉計算機了。開始選單中的這些選項也會被禁用。

最後還要介紹一個函式:IsTokenRestricted。這個函式不會告訴你該訪問令牌是否是由CreateRestrictedToken 建立的,但卻會告訴你該訪問令牌是否包含受限SID。因此,除非你要使用受限SID,否則這個函式並沒有什麼太大用處。

 

完整性級別(Integrity levels)

UAC提供了個很少有人注意到的特性,那就是強制完整性控制(Mandatory Integrity Control)。這是一個新的新增到程式和安全描述符(security descriptor)上的授權特性。我們可以為需要安全保護的資源在其安全描述符中指定一個完整性級別。系統中的每個程式也有相應的完整性級別標記,然後即可與資源的完整性級別相互驗證,並提供額外的安全保護。這不但非常簡單,也是個極為有用的特性,能夠幫助你簡單有效地將程式的可訪問資源分隔開來。

設想作為開發者的你需要開發一個應用程式,該應用程式必須處理從無法信任的源(例如Internet)中獲取的資料。因為資料中可能包含有惡意程式碼,所以你必須想方設法保護計算機的安全,因此為你的程式新增一個“深度防禦(defense in depth)”層就顯得非常有用。其中一個非常有效的解決方案就是使用前面一節中描述的受限訪問令牌。但是這種解決方案可能會很複雜,因為你需要明確地指出哪些資源的哪些SID可以被允許、哪些SID需要被禁用,考慮到程式本身也需要一定的許可權來正常執行,你不得不做出大量的授權工作。這正是引入完整性級別的意義所在。完整性級別一般用來阻止寫訪問,而允許讀訪問和執行。而有了讀和執行的許可權,程式基本上即可完成大部分的工作,而阻止了寫許可權則可以限制其對系統的危害,例如覆蓋系統檔案或修改某些路徑資訊等。這也正是IE 7的實現方式。IE 7的部分功能執行於一個低完整性級別的獨立的程式中,只允許程式修改少數幾個位置的檔案。

使用者態程式可以設定為如下四種完整性級別:

  1. Low
  2. Medium
  3. High
  4. System

標準使用者訪問令牌以及受限(未經提升過)的管理員訪問令牌擁有中等(Medium)的完整性級別。不受限制(經過提升)的管理員訪問令牌擁有高(High)完整性級別。執行於Local System之下的帳號擁有系統(System)完整性級別。IE程式的完整性級別則為低(Low)。有一種很簡單的查勘程式完整性級別的方法:用最新版本的Process Explorer,它提供了一個可選的列,可以顯示出每個程式的完整性級別。

按照預設,子程式將繼承父程式的完整性級別。在建立程式時我們可以更改其完整性級別,但一旦建立完畢就不能再更改。另外,我們也不能將子程式的完整性級別設定得高於父程式。這可以阻止低完整性級別的程式藉機會竊取更高的完整性級別。

讓我們首先看一下如何查詢並設定某一程式的完整性級別,然後再討論如何為需要保護的資源設定完整性級別。

 

程式完整性級別(Process integrity levels)

我們可以通過檢查程式的訪問令牌來取得其完整性級別資訊。GetTokenInformation 函式可以返回不同種類的資訊。例如,若想通過訪問令牌圖的當前的使用者帳號,我們可以指定TokenUser 類,然後,GetTokenInformation 函式將基於該訪問令牌生成一個TOKEN_USER 結構。類似地,使用TokenIntegrityLevel 類器可查詢該程式的完整性級別,隨後將返回TOKEN_MANDATORY_LABEL 結構。大多數GetTokenInformation 返回的結構體的長度都是可變的,因為只有GetTokenInformation 函式本身才知道到底需要多少記憶體空間,所以我們在呼叫時必須格外小心。因為大多數底層的安全相關函式均使用LocalAlloc LocalFree 來分配/釋放記憶體,所以我使用了一個名為LocalMemory 的類别範本和一個GetTokenInformation 函式模板來簡化所需要的工作,該類可以在本文的下載程式碼中找到。這裡我們先把注意力放在手頭的主題上:

CHandle processToken;
VERIFY(::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_QUERY,
                          &processToken.m_h));
LocalMemory<PTOKEN_MANDATORY_LABEL>info;
COM_VERIFY(GetTokenInformation(processToken,
                               TokenIntegrityLevel,
                               info));
SID* sid = static_cast<SID*>(info->Label.Sid);
DWORD rid = sid->SubAuthority[0];
switch (rid)
{
    case SECURITY_MANDATORY_LOW_RID:
    {
        // Low integrity process
        break;
    }
    case SECURITY_MANDATORY_MEDIUM_RID:
    {
        // Medium integrity process
        break;
    }
    case SECURITY_MANDATORY_HIGH_RID:
    {
        // High integrity process
        break;
    }
    case SECURITY_MANDATORY_SYSTEM_RID:
    {
        // System integrity level
        break;
    }
    default:
    {
        ASSERT(false);
    }
}

這裡我們使用了OpenProcessToken 來取得需要查詢的程式的訪問令牌。然後呼叫了我編寫的GetTokenInformation 函式模版(當然,提供了適當的類的資訊),並用LocalMemory 類别範本指定了資訊的型別。函式返回的TOKEN_MANDATORY_LABEL 結構包含了表示完整性級別的SID。分析這個SID即可得到我們想要的表示完整性級別的相對標示符(relative identifier,RID)。

設定子程式的完整性級別非常直觀簡單。首先複製一份父程式的訪問令牌,然後使用前面例項程式中用來查詢完整性級別的那些資訊類和資料結構設定其完整性級別。這時即可使用SetTokenInformation 函式。最後呼叫CreateProcessAsUser 函式,並使用修改過的訪問令牌即可建立出需要的子程式。請參考下述程式碼:

CHandle processToken;
VERIFY(::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_DUPLICATE,
                          &processToken.m_h));
CHandle duplicateToken;
VERIFY(::DuplicateTokenEx(processToken,
                          MAXIMUM_ALLOWED,
                          0, // token attributes
                          SecurityAnonymous,
                          TokenPrimary,
                          &duplicateToken.m_h));
WellKnownSid integrityLevelSid(WellKnownSid::MandatoryLabelAuthority,
                               SECURITY_MANDATORY_LOW_RID);
TOKEN_MANDATORY_LABEL tokenIntegrityLevel = { 0 };
tokenIntegrityLevel.Label.Attributes = SE_GROUP_INTEGRITY;
tokenIntegrityLevel.Label.Sid = &integrityLevelSid;
VERIFY(::SetTokenInformation(duplicateToken,
                             TokenIntegrityLevel,
                             &tokenIntegrityLevel,
                             sizeof (TOKEN_MANDATORY_LABEL) + ::GetLengthSid(&integrityLevelSid)));
STARTUPINFO startupInfo = { sizeof (STARTUPINFO) };
ProcessInfo processInfo;
VERIFY(::CreateProcessAsUser(duplicateToken,
                             L"C:\Windows\Notepad.exe",
                             0, // cmd line
                             0, // process attributes
                             0, // thread attributes
                             FALSE, // don`t inherit handles
                             0, // flags
                             0, // inherit environment
                             0, // inherit current directory
                             &startupInfo,
                             &processInfo));

這個例項程式將開啟記事本程式。你會注意到,雖然該記事本程式可以開啟大多數位置中的文字檔案,但卻無法儲存至任何位置,因為它的完整性級別為低。

還要說一句,我們可以使用LookupAccountSid 函式得到完整性級別的可顯示名稱,但該函式的返回值對使用者卻並不是那麼友好,所以你最好另外設定一個字串表,包含類似“低”、“中等”、“高”以及“系統”等文字。

系統為標準使用者建立的訪問令牌的完整性級別為中等。系統為管理員建立的受限訪問令牌的完整性級別也是中等,但未受限管理員訪問令牌的完整性級別為高。

現在讓我們看看如何為指定的資源設定完整性級別。

 

資源完整性級別(Resource integrity levels)

資源的完整性級別存放在資源安全描述符的系統訪問控制表(ystem access control list,SACL)中的一個特殊的訪問控制條目(access control entry,ACE)中。更新該值的最簡單方法就是使用SetNamedSecurityInfo 函式。Windows Vista還提供了一個新的名為AddMandatoryAce 的函式,用來將一類特殊的ACE(強制ACE)新增至ACL中。記住,安全相關的縮寫詞總是會讓人一頭霧水……認真地說,若你熟悉安全描述符相關程式設計的話,那麼這段程式碼看起來將相當簡單。首先使用InitializeAcl 函式準備了一個足夠容納一個單獨ACE的ACL。接下來建立用SID表示的完整性級別,並使用AddMandatoryAce 函式將其新增至ACL中。最後使用SetNamedSecurityInfo 函式更新完整性級別。注意在下面的程式碼中,我們使用了一個新的LABEL_SECURITY_INFORMATION標記:

LocalMemory<PACL> acl;
const DWORD bufferSize = 64;
COM_VERIFY(acl.Allocate(bufferSize));
VERIFY(::InitializeAcl(acl.m_p,
                       bufferSize,
                       ACL_REVISION));
WellKnownSid sid(WellKnownSid::MandatoryLabelAuthority,
                 SECURITY_MANDATORY_LOW_RID);
COM_VERIFY(Kerr::AddMandatoryAce(acl.m_p,
                                 &sid));
CString path = L"C:\SampleFolder";
DWORD result = ::SetNamedSecurityInfo(const_cast<PWSTR>(path.GetString()),
                                      SE_FILE_OBJECT,
                                      LABEL_SECURITY_INFORMATION,
                                      0, // owner
                                      0, // group
                                      0, // dacl
                                      acl.m_p); // sacl
ASSERT(ERROR_SUCCESS == result);

得到資源的完整性級別也非常簡單,可以看到大多數資源並沒有顯式地設定其完整性級別。系統將沒有顯式宣告完整性級別的資源看作帶有中等完整性級別。首先使用同樣的安全資訊標記LABEL_SECURITY_INFORMATION呼叫GetNamedSecurityInfo 函式。然後即可簡單地通過GetAce 函式得到指向儲存了完整性級別SID的ACE的指標,隨後通過讀取其RID值即可判斷其完整性級別。下面是一段示例:

CString path = L"C:\SampleFolder";
LocalMemory<PSECURITY_DESCRIPTOR>descriptor;
PACL acl = 0;
DWORD result = ::GetNamedSecurityInfo(const_cast<PWSTR>(path.GetString()),
                                      SE_FILE_OBJECT,
                                      LABEL_SECURITY_INFORMATION,
                                      0,
                                      0,
                                      0,
                                      &acl,
                                      &descriptor.m_p);
ASSERT(ERROR_SUCCESS == result);
DWORD integrityLevel = SECURITY_MANDATORY_MEDIUM_RID;
if (0 != acl && 0 < acl->AceCount)
{
    ASSERT(1 == acl->AceCount);
    SYSTEM_MANDATORY_LABEL_ACE* ace = 0;
    VERIFY(::GetAce(acl,
                    0,
                    reinterpret_cast<void**>(&ace)));
    ASSERT(0 != ace);
    SID* sid = reinterpret_cast<SID*>(&ace->SidStart);
    integrityLevel = sid->SubAuthority[0];
}
ASSERT(SECURITY_MANDATORY_LOW_RID == integrityLevel);

 

以管理員身份執行(Run as Administrator)

目前為止,我們已經注意分析了組成UAC的各個部分,例如受限訪問令牌和完整性級別等。接下來讓我們看看“以管理員身份執行”是什麼意思,我們如何以程式設計方式實現這個功能。或許你已經注意到了,在Windows Vista中你可以右鍵單擊某個應用程式或快捷方式圖示,並在彈出的上下文選單中選擇“以管理員身份執行”。無論是管理員還是標準使用者,Vista都提供了這個選項。以管理員身份執行的概念可以簡單地理解為作了一次“提升”或是建立一個“提升”了的程式。若想“以管理員身份執行”,那麼標準使用者需要輸入管理員的使用者名稱和密碼,而管理員則需要在彈出對話方塊中進行一次確認。無論那種情況,結果都是一樣的:系統將建立一個擁有不受限制管理員許可權的新的程式,該程式擁有系統所有的許可和許可權。

程式的“提升”顯得有些複雜,但幸運的是,大多數複雜性都被隱藏在更新版本的ShellExecute(Ex) 函式中了。Windows Vista中的ShellExecute 函式通過一個非公開的COM介面使用新的應用程式資訊服務(Application Information,appinfo)來執行提升操作。ShellExecute 首先呼叫CreateProcess ,嘗試建立一個新的程式。CreateProcess 負責包括檢查應用程式相容性設定、應用程式清單(application manifest)以及執行時載入器(runtime loader)等任務。若CreateProcess 發現應用程式需要一個“提升”而其呼叫程式卻沒有提升的話,則函式呼叫會以ERROR_ELEVATION_REQUIRED失敗告終。然後ShellExecute 呼叫應用程式資訊服務來處理提升操作並建立被“提升”過的程式,因為呼叫程式顯然沒有執行該任務所需要的足夠的許可權。最後,應用程式資訊服務呼叫CreateProcessAsUser 以獲得必需的非限制的管理員訪問令牌。

還有一種方法:若你只想要一個經過“提升”了的程式,而不關心使用哪個應用程式資訊服務的話,那麼只要在ShellExecute中使用這個鮮為人知的“runas”就可以了。無論應用程式清單或相容性資訊有多麼變態,這個命令均可以實現“提升”功能。實際上,runas並不是Windows Vista中的新東西。在Windows XP和Windows 2003中就已經出現了,常用在通過shell直接建立受限訪問令牌。可是在Vista中,它的行為卻有了些變化,請參考如下的示例程式:

::ShellExecute(0, // owner window
               L"runas",
               L"C:\Windows\Notepad.exe",
               0, // params
               0, // directory
               SW_SHOWNORMAL);

想想系統在後臺默默地為你做了多少工作吧!只要這麼一行程式碼就搞定了如此複雜的功能,是不是覺得很爽呢?雖然建立一個“提升”過的程式有時候顯得很合理,但若你只想暫時提升一下的話,或許這並不是最恰當的解決方案。讓我們接下來看看如何通過提升COM物件完成同樣的工作。

 

建立一個被“提升”了的COM物件

若你對COM有所造詣的話,應該知道COM支援我們在一個代理程式中建立COM伺服器。現在這項技術又有了一些發展,我們可以在一個“提升”了的代理程式中建立COM伺服器了。這項技術非常有用,藉助於它的幫助,我們就可以在應用程式執行期間簡單地建立一個COM物件,而不必去建立一個全新的程式。

使用這個技術中最難的一部分就是如何正確地註冊該COM伺服器,保證將其載入到一個“提升”了的代理程式中,因為COM物件需要我們顯式地宣告其協作方式。

我們要做的第一件事就是更新COM的註冊,用來保證我們的庫(DLL)伺服器能夠執行於一個代理程式中。只要將“DllSurrogate”新增至伺服器的AppID登錄檔鍵中即可。在ATL中,只要簡單地更新專案的主RGS檔案,如下所示:

HKCR
{
    NoRemove AppID
    {
        `%APPID%` = s `SampleServer`
        {
            val DllSurrogate = s ``
        }
        `SampleServer.DLL`
        {
            val AppID = s `%APPID%`
        }
    }
}

DllSurrogate 的空值表示系統提供的代理程式即刻可以使用。現在COM客戶端就能夠指定CLSCTX_LOCAL_SERVER 執行上下文,在該代理程式中建立COM伺服器了:

CComPtr<ISampleServer>server;
COM_VERIFY(server.CoCreateInstance(__uuidof(SampleServer),
                                   0,
                                   CLSCTX_LOCAL_SERVER));

下一步就是啟用該COM類的“提升”執行。這需要我們在該COM類的註冊指令碼中新增一些東西——一個用來表示支援“提升”的提升鍵,以及一個名為“LocalizedString”的值,加上用來顯示在UAC確認對話方塊中的名稱。ATL中COM類的註冊指令碼將類似如下所示:

HKCR
{
    SampleServer.SampleServer.1 = s `SampleServer Class`
    {
        CLSID = s `{91C5423A-CF90-4E62-93AD-E5B922AE8681}`
    }
    SampleServer.SampleServer = s `SampleServer Class`
    {
        CLSID = s `{91C5423A-CF90-4E62-93AD-E5B922AE8681}`
        CurVer = s `SampleServer.SampleServer.1`
    }
    NoRemove CLSID
    {
        ForceRemove {91C5423A-CF90-4E62-93AD-E5B922AE8681} = s `SampleServer Class`
        {
            ProgID = s `SampleServer.SampleServer.1`
            VersionIndependentProgID = s `SampleServer.SampleServer`
            InprocServer32 = s `%MODULE%`
            {
                val ThreadingModel = s `Neutral`
            }
            val AppID = s `%APPID%`
            `TypeLib` = s `{A43B074B-0452-4FF4-8308-6B0BF641C3AE}`
            Elevation
            {
                val Enabled = d 1
            }
            val LocalizedString = s `@%MODULE%,-101`
        }
    }
}

不要忘記在你的字串表中為本地化名稱新增一個條目。現在該COM客戶即可啟動該“提升”了的COM伺服器了。CoCreateInstance 不像CreateProcess那樣直接建立一個“提升”了的COM物件,我們需要使用“COM提升名稱(COM elevation moniker)”來完成。最簡單的方法就是使用CoGetObject 函式建立這個名稱(moniker )並返回最終建立好的物件的代理:

template <typename T>
HRESULT CreateElevatedInstance(HWND window,
                               REFCLSID classId,
                               T** object)
{
    BIND_OPTS3 bindOptions;
    ::ZeroMemory(&bindOptions, sizeof (BIND_OPTS3));
    bindOptions.cbStruct = sizeof (BIND_OPTS3);
    bindOptions.hwnd = window;
    bindOptions.dwClassContext = CLSCTX_LOCAL_SERVER;
    CString string;
    const int guidLength = 39;
    COM_VERIFY(::StringFromGUID2(classId,
                                 string.GetBufferSetLength(guidLength),
                                 guidLength));
    string.ReleaseBuffer();
    string.Insert(0, L"Elevation:Administrator!new:");
    return ::CoGetObject(string,
                         &bindOptions,
                         __uuidof(T),
                         reinterpret_cast<void**>(object));
}
Using the function template is just as simple as calling CoCreateInstance:
CComPtr<ISampleServer>server;
COM_VERIFY(CreateElevatedInstance(0, // window
                                  __uuidof(SampleServer),
                                  &server);

如同ShellExecute一樣,UAC使用該窗體的控制程式碼判斷該提示是否會得到輸入焦點,還是隻在後臺默默等待。

 

使用應用程式清單(application manifests)

還記得我曾經提到過CreateProcess 會檢查應用程式相容性設定以及應用程式清單麼?確實如此,Windows Vista為了確保遺留的32位應用程式能夠正常執行而做了很多努力。以往的應用程式可以輕鬆地完全控制檔案系統以及登錄檔,而為了讓其也能夠在UAC所提供的更加嚴格的執行環境中繼續可用,Vista作了令人難以想象的大量的模擬工作。但儘管如此,其核心理念還是儘可能地避免這些模擬。這種模擬只對那些遺留的應用程式有意義,如果你現在開發新的應用程式的話,那麼請確保提供相應的應用程式清單,以避免這類不必要的模擬。微軟公司也在計劃在後續版本中的Windows中刪除對這些模擬的支援。

應用程式清單的架構在Windows Vista下有所更新,應用程式可以在該清單中給出其需要的安全性上下文。但令人不爽的是,Visual C++ 會自動生成應用程式清單。實際上這是件好事。聯結器始終對應用程式的各個依賴保持清醒,而應用程式清單則用來定義並行程式集之間的依賴。幸運的是,Visual C++ 也提供了將額外的應用程式清單與現有的清單合併的選項,並能夠將合併後的整體清單一起嵌入到應用程式的可執行檔案中。Visual C++ 專案的“Additional Manifest Files”設定正是為此而設。下面就是一份示例應用程式清單,其中宣告瞭對Common Controls 6.0的依賴,並指定了其希望得到的安全性上下文:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        type="win32"
        name="Microsoft.Windows.Common-Controls"
        version="6.0.0.0"
        processorArchitecture="*"
        publicKeyToken="6595b64144ccf1df"
        language="*"
        />
    </dependentAssembly>
  </dependency>
  <v3:trustInfo xmlns:v3="urn:schemas-microsoft-com:asm.v3">
    <v3:security>
      <v3:requestedPrivileges>
        <!-- level can be "asInvoker", "highestAvailable", or "requireAdministrator" -->
        <v3:requestedExecutionLevel level="highestAvailable" />
      </v3:requestedPrivileges>
    </v3:security>
  </v3:trustInfo>
</assembly>

requestedExecutionLevel可以指定為三個值:

  1. asInvoker:預設選項,新的程式將簡單地繼承其父程式的訪問令牌。
  2. highestAvailable:應用程式會選擇該使用者允許範圍內儘可能寬鬆的安全上下文。對於標準使用者來說,該選項與asInvoker一樣,而對於管理員來說,這就意味著請求非限制訪問令牌。
  3. requireAdministrator:應用程式需要管理員的非限制訪問令牌。執行該程式時,標準使用者將要輸入管理員的使用者名稱和密碼,而管理原則要在彈出的確認對話方塊中進行確認。

需要記住的是,CreateProcess 將會檢查應用程式清單,如果所請求的執行級別高與其父程式的完整性級別,那麼這次呼叫將失敗。只有ShellExecute 才會使用應用程式資訊服務來執行“提升”。

 

我真的被“提升”了麼?

如果你想要知道現在是否已經被“提升”過,那麼簡單地呼叫IsUserAnAdmin 函式即可。如果你還嫌不夠精確,那麼也可以使用GetTokenInformation 函式,但大多數情況下似乎都有些高射炮打蚊子——大材小用了。

 

結論

這就使我能講出的關於UAC的一切,希望能夠對你有所幫助。這篇文章中的內容基本都不在官方文件中,因此改變也是在所難免的。

再說一句,本文示例程式中我大多使用了斷言(assertion)及類似的巨集來檢查可能發生的異常。這麼做是為了判斷哪些地方應該使用異常處理。如果你想在應用程式中使用部分其中的程式碼,那麼請確保將這些巨集替換成適合你自己的處理機制,無論是異常、HRESULT還是別的什麼東西。

本文的下載程式碼中包含了一些輔助類别範本以及輔助函式,希望能對你有所幫助!


相關文章