Windows AppContainer 降權,隔離與安全 頂 原

##Windows 權限策略的發展 從 Windows 8開始,我在使用 Windows 系統的同時也就不再關閉 UAC 了,並且不再安裝任何國產的安全軟件,這些軟件仗着運行在管理員權限上肆意的推行 “全家桶策略”,Windows 多年來一直是最流行的操作系統,大多數人的焦點都會放在上面,也包括黑客,各種企業,早期Windows系統在權限的管理上非常粗放。 無論是惡意軟件還是其他軟件都可以獲得較高的權限,這樣就能夠對系統大肆修改,並且直接或間接破化系統,收集數據,妨礙競爭對手。
軟件的權限理應得到限制,而不是放任自流。 Windows XP 是歷史上最受歡迎的版本之一,然而,一直以來XP的權限問題都被人詬病,微軟也決心對這一問題進行改進,從Vista開始,Windows引入了 UAC 機制, 它要求用戶在執行可能會影響計算機運行的操作或執行更改影響其他用戶的設置的操作之前,提供權限或管理員‌密碼。這是一個可喜的進步,不過在早期用戶都會 要求關閉 UAC,當我開始使用 Windows 的時候,,那個時候用的是 Windows 7,我也是這樣做的。Windows 7在 UAC 的改進主要是一些小的細節。
從 Windows 8 引入 Metro(Morden) App 開始,Windows 出現了一個新的進程隔離機制,即 AppContainer。Windows Store 應用運行在 AppContainer 的容器當中 權限被極大的限制,很多危險的操作無法進行,微軟通過 Windows Store 進行應用分發,能夠控制來源,這樣能夠極大的降低惡意軟件的困擾。
而 AppContainer 同樣能夠支持傳統的 Desktop 應用,本文將介紹 通過 AppContainer 啓動一個桌面程序,當然,先從降權說起。

##UAC 降權 基於 Win32 的應用程序,如果要提權,非常簡單,第一可以在 manifest 文件中寫入 'requireAdministrator' Visual Studio 項目屬性中可以設置。

<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity version="1.0.3.0" name="Force.Metro.Native.iBurnMgr.app"/>
 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
      </requestedPrivileges>
    </security>
  </trustInfo>
</asmv1:assembly>

第二,可以使用 ShellExecute,將第二個參數設置爲 “runas” 即可,ShellExecute 本質上將參數寫入到 SHELLEXECUTEINFO ,然後調用 ShellExecuteEx 實現。 在 ReactOS 和 Windows Vista 以前的版本,通過 IShellExecuteHook 接口實現。Vista 以後被否決了。

當然,還可以有其他不受官方建議的方法。

但是如果需要降權,微軟沒有直接的方案供開發者選擇。常見的選擇有 通過拿到 Explorer 的 token 啓動進程,或者是通過計劃任務啓動進程。

##計劃任務降權 使用計劃任務降權大概需要十幾個 COM 接口:

  ITaskService *iTaskService = nullptr;
  ITaskFolder *iRootFolder = nullptr;
  ITaskDefinition *iTask = nullptr;
  IRegistrationInfo *iRegInfo = nullptr;
  IPrincipal *iPrin = nullptr;
  ITaskSettings *iSettings = nullptr;
  ITriggerCollection *iTriggerCollection = nullptr;
  ITrigger *iTrigger = nullptr;
  IRegistrationTrigger *iRegistrationTrigger = nullptr;
  IActionCollection *iActionCollection = nullptr;
  IAction *iAction = nullptr;
  IExecAction *iExecAction = nullptr;
  IRegisteredTask *iRegisteredTask = nullptr;

使用這些接口創建一個計劃任務,值得注意的是,計劃任務的創建需要管理員權限運行,本程序的功能就是從管理員降權到標準用戶,所以這個限制沒有影響。
在這個過程中最有價值的代碼是:

 DO(iPrin->put_RunLevel(TASK_RUNLEVEL_LUA))

MSDN 的描述中,表示以較低權限運行,與之對應的是 “TASK_RUNLEVEL_HIGHEST”。 通過計劃任務降權的完整代碼: UAC 降權測試 從 Process Explorer 的進程屬性就可以看到: TaskSchdLauncher 如果用戶名是內置的 Administrator,並且開啓了 對內置管理員使用批准模式 至少在Windows 8.1 (Windows Server 2012 R2)會失敗的。 所以這個時候需要採取第二種策略。

##使用 Explorer 的 Token 啓動進程 簡單點就是拿桌面進程的 Token,然後使用桌面的 Token 啓動進程。這需要桌面正在運行 (Explorer 作爲桌面進程正在運行),也就是常說的桌面得存在,並且權限是標準的。

HRESULT WINAPI ProcessLauncherExplorerLevel(LPCWSTR exePath,LPCWSTR cmdArgs,LPCWSTR workDirectory)
{
  STARTUPINFOW si;
  PROCESS_INFORMATION pi;
  SecureZeroMemory(&si, sizeof(si));
  SecureZeroMemory(&pi, sizeof(pi));
  si.cb = sizeof(si);
  HANDLE hShellProcess = nullptr, hShellProcessToken = nullptr,
  hPrimaryToken = nullptr;
  HWND hwnd = nullptr;
  DWORD dwPID = 0;
  HRESULT hr = S_OK;
  BOOL ret = TRUE;
  DWORD dwLastErr;
  hwnd = GetShellWindow();
  if (nullptr == hwnd) {
    return HRESULT(3);
  }

  GetWindowThreadProcessId(hwnd, &dwPID);
  if (0 == dwPID) {
    return HRESULT(4);
  }

  // Open the desktop shell process in order to query it (get the token)
  hShellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwPID);
  if (!hShellProcess) {
    dwLastErr = GetLastError();
    return HRESULT(5);
  }

  // From this point down, we have handles to close, so make sure to clean up.

  bool retval = false;
  // Get the process token of the desktop shell.
  ret = OpenProcessToken(hShellProcess, TOKEN_DUPLICATE, &hShellProcessToken);
  if (!ret) {
    dwLastErr = GetLastError();
    hr = HRESULT(6);
    goto cleanup;
  }

  // Duplicate the shell's process token to get a primary token.
  // Based on experimentation, this is the minimal set of rights required for
  // CreateProcessWithTokenW (contrary to current documentation).
  const DWORD dwTokenRights = TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY |
                              TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT |
                              TOKEN_ADJUST_SESSIONID;
  ret = DuplicateTokenEx(hShellProcessToken, dwTokenRights, nullptr,
                         SecurityImpersonation, TokenPrimary, &hPrimaryToken);
  if (!ret) {
    dwLastErr = GetLastError();
    hr = 7;
    goto cleanup;
  }
  // Start the target process with the new token.
  wchar_t *cmdArgsT = _wcsdup(cmdArgs);
  ret = CreateProcessWithTokenW(hPrimaryToken, 0, exePath, cmdArgsT, 0, nullptr,
                                workDirectory, &si, &pi);
  free(cmdArgsT);
  if (!ret) {
    dwLastErr = GetLastError();
    hr = 8;
    goto cleanup;
  }
  CloseHandle(pi.hThread);
  CloseHandle(pi.hProcess);
  retval = true;

cleanup:
  // Clean up resources
  CloseHandle(hShellProcessToken);
  CloseHandle(hPrimaryToken);
  CloseHandle(hShellProcess);
  return hr;
}

當然,通過人肉合成一個 Token 啓動進程也是能夠實現降低程序權限的,這些比較複雜,本文也就不細說了。

##啓動低完整性進程 強制完整性控制(英語:Mandatory Integrity Control)是一個在微軟Windows操作系統中從Windows Vista開始引入,並沿用到後續版本系統的核心安全功能。強制完整性控制通過完整性級別標籤來爲運行於同一登錄會話的進程提供隔離。此機制的目的是在一個潛在不可信的上下文(與同一賬戶下運行的其他較爲可信的上下文相比)中選擇性地限制特定進程和軟件組件的訪問權限。 Windows Vista 定義了四個完整性級別:

低 (SID: S-1-16-4096)
中 (SID: S-1-16-8192)
高 (SID: S-1-16-12288)
系統 (SID: S-1-16-16384)

利用這一特性,我們可以使用低級別權限啓動一個進程:

#include <Windows.h>
#include <Sddl.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <wchar.h>
#include <iostream>


#pragma comment(lib,"kernel32")
#pragma comment(lib,"Advapi32")
#pragma comment(lib,"user32")

BOOL WINAPI CreateLowLevelProcess(LPCWSTR lpCmdLine) {
  BOOL b;
  HANDLE hToken;
  HANDLE hNewToken;
  // PWSTR szProcessName = L"LowClient";
  PWSTR szIntegritySid = L"S-1-16-4096";
  PSID pIntegritySid = NULL;
  TOKEN_MANDATORY_LABEL TIL = {0};
  PROCESS_INFORMATION ProcInfo = {0};
  STARTUPINFOW StartupInfo = {0};
  StartupInfo.cb=sizeof(STARTUPINFOW);
  ULONG ExitCode = 0;

  b = OpenProcessToken(GetCurrentProcess(), MAXIMUM_ALLOWED, &hToken);
  if(!b)
      return FALSE;
  b = DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation,
                       TokenPrimary, &hNewToken);
  b = ConvertStringSidToSidW(szIntegritySid, &pIntegritySid);
  TIL.Label.Attributes = SE_GROUP_INTEGRITY;
  TIL.Label.Sid = pIntegritySid;

  // Set process integrity levels
  b = SetTokenInformation(hNewToken, TokenIntegrityLevel, &TIL,
                          sizeof(TOKEN_MANDATORY_LABEL) +
                              GetLengthSid(pIntegritySid));

  // Set process UI privilege level
  /*b = SetTokenInformation(hNewToken, TokenIntegrityLevel,
  &TIL, sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(pIntegritySid)); */
  wchar_t *lpCmdLineT = _wcsdup(lpCmdLine);
  // To create a new low-integrity processes
  b = CreateProcessAsUserW(hNewToken, NULL, lpCmdLineT, NULL, NULL, FALSE, 0,
                          NULL, NULL, &StartupInfo, &ProcInfo);
  CloseHandle(hToken);  
  CloseHandle(hNewToken);
  CloseHandle(ProcInfo.hThread);
  CloseHandle(ProcInfo.hProcess);
  LocalFree(pIntegritySid);
  free(lpCmdLineT);
  return b;
}

int wmain(int argc,wchar_t *argv[])
{
    if(argc>=2)
    {
        std::wcout<<L"Start LowLevel App: "<<argv[1]<<L"\t Return Code[BOOL]: "<<CreateLowLevelProcess(argv[1])<<std::endl;
    }
    return 0;
}

第一步獲得當前進程的 Token ,然後使用這個令牌創建一個新的令牌,由 SID "S-1-16-4096" 得到一個 SID 指針,將 SID 指針添加到 TOKEN_MANDATORY_LABEL 結構中,而後用SetTokenInformation將令牌與 完整性級別結合在一起,最後使用CreateProcessAsUser 創建進程。通過完整性級別啓動的進程是沒有多少權限的,譬如打開一個記事本,新建一個文件另存爲,基本上都無法寫入。 使用 Process Explorer 可以查看啓動進程的權限屬性。

MIC

強制完整性運用最多的應該是 IE 瀏覽器,從 IE8 開始,IE 瀏覽器的保護模式就是 MIC,而 MIC 是 Windows 權限細粒度的一次重大的發展,在前幾年,在學校開發 ACM 在線測評系統之時,評測系統就是基於 MIC+Job Object 實現的。

##AppConatiner 從 Windows 8 開始,微軟引入了新的安全機制,AppConatiner 所有的 Store App 就是運行在應用容器之中,並且 IETab 也是運行在應用容器之中,應用容器在權限的管理上非常細緻,也就是說非常“細粒度”。 微軟也爲傳統的Desktop應用程序提供了一系列的API來創建一個AppContainer,並且使進程在AppContainer中啓動。比如使用CreateAppContainerProfile創建一個容器SID,使用DeleteAppContainerProfile查找一個已知容器名的SID,刪除一個容器DeleteAppContainerProfile配置文件。GetAppContainerFolderPath 獲得容器目錄。

通過 AppContainer 啓動進程的一般流程是,通過 CreateAppContainerProfile 創建一個容器配置,得到 SID 指針,爲了避免創建失敗,先用 DeleteAppContainerProfile 刪除此容器配置。細粒度的配置需要 WELL_KNOWN_SID_TYPE
得到容器配置後,啓動進程時需要使用 STARTUPINFOEX 結構,使用 InitializeProcThreadAttributeList UpdateProcThreadAttribute 將 PSID 和 SECURITY_CAPABILITIES::Capabilities (也就是 WELL_KNOWN_SID_TYPE 得到的權限設置)添加到 STARTUPINFOEX::lpAttributeList 使用 CreateProcess 中第七個參數 添加 EXTENDED_STARTUPINFO_PRESENT,然後再用 reinterpret_cast 轉換 STARTUPFINFOEX 指針變量輸入到 CreateProcess 倒數第二個(C語言用強制轉換)。

下面是一個完整的例子。

#include <vector>
#include <memory>
#include <type_traits>
#include <Windows.h>
#include <sddl.h>
#include <Userenv.h>
#include <iostream>

#pragma comment(lib,"Userenv")
#pragma comment(lib,"Shlwapi")
#pragma comment(lib,"kernel32")
#pragma comment(lib,"user32")
#pragma comment(lib,"Advapi32")
#pragma comment(lib,"Ole32")
#pragma comment(lib,"Shell32")

typedef std::shared_ptr<std::remove_pointer<PSID>::type> SHARED_SID;

bool SetCapability(const WELL_KNOWN_SID_TYPE type, std::vector<SID_AND_ATTRIBUTES> &list, std::vector<SHARED_SID> &sidList) {
  SHARED_SID capabilitySid(new unsigned char[SECURITY_MAX_SID_SIZE]);
  DWORD sidListSize = SECURITY_MAX_SID_SIZE;
  if (::CreateWellKnownSid(type, NULL, capabilitySid.get(), &sidListSize) == FALSE) {
    return false;
  }
  if (::IsWellKnownSid(capabilitySid.get(), type) == FALSE) {
    return false;
  }
  SID_AND_ATTRIBUTES attr;
  attr.Sid = capabilitySid.get();
  attr.Attributes = SE_GROUP_ENABLED;
  list.push_back(attr);
  sidList.push_back(capabilitySid);
  return true;
}

static bool MakeWellKnownSIDAttributes(std::vector<SID_AND_ATTRIBUTES> &capabilities,std::vector<SHARED_SID> &capabilitiesSidList)
{

    const WELL_KNOWN_SID_TYPE capabilitiyTypeList[] = {
        WinCapabilityInternetClientSid, WinCapabilityInternetClientServerSid, WinCapabilityPrivateNetworkClientServerSid,
        WinCapabilityPicturesLibrarySid, WinCapabilityVideosLibrarySid, WinCapabilityMusicLibrarySid,
        WinCapabilityDocumentsLibrarySid, WinCapabilitySharedUserCertificatesSid, WinCapabilityEnterpriseAuthenticationSid,
        WinCapabilityRemovableStorageSid,
    };
    for(auto type:capabilitiyTypeList) {
        if (!SetCapability(type, capabilities, capabilitiesSidList)) {
            return false;
        }
    }
    return true;
}


HRESULT AppContainerLauncherProcess(LPCWSTR app,LPCWSTR cmdArgs,LPCWSTR workDir)
{
    wchar_t appContainerName[]=L"Phoenix.Container.AppContainer.Profile.v1.test";
    wchar_t appContainerDisplayName[]=L"Phoenix.Container.AppContainer.Profile.v1.test\0";
    wchar_t appContainerDesc[]=L"Phoenix Container Default AppContainer Profile  Test,Revision 1\0";
    DeleteAppContainerProfile(appContainerName);///Remove this AppContainerProfile
    std::vector<SID_AND_ATTRIBUTES> capabilities;
    std::vector<SHARED_SID> capabilitiesSidList;
    if(!MakeWellKnownSIDAttributes(capabilities,capabilitiesSidList))
        return S_FALSE;
    PSID sidImpl;
    HRESULT hr=::CreateAppContainerProfile(appContainerName,
        appContainerDisplayName,
        appContainerDesc,
        (capabilities.empty() ? NULL : &capabilities.front()), capabilities.size(), &sidImpl);
    if(hr!=S_OK){
        std::cout<<"CreateAppContainerProfile Failed"<<std::endl;
        return hr;
    }
    wchar_t *psArgs=nullptr;
    psArgs=_wcsdup(cmdArgs);
    PROCESS_INFORMATION pi;
    STARTUPINFOEX siex = { sizeof(STARTUPINFOEX) };
    siex.StartupInfo.cb = sizeof(STARTUPINFOEXW);
    SIZE_T cbAttributeListSize = 0;
    BOOL bReturn = InitializeProcThreadAttributeList(
        NULL, 3, 0, &cbAttributeListSize);
    siex.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, cbAttributeListSize);
    bReturn = InitializeProcThreadAttributeList(siex.lpAttributeList, 3, 0, &cbAttributeListSize);
    SECURITY_CAPABILITIES sc;
    sc.AppContainerSid = sidImpl;
    sc.Capabilities = (capabilities.empty() ? NULL : &capabilities.front());
    sc.CapabilityCount = capabilities.size();
    sc.Reserved = 0;
    if(UpdateProcThreadAttribute(siex.lpAttributeList, 0,
        PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES,
        &sc,
        sizeof(sc) ,
        NULL, NULL)==FALSE)
    {
        goto Cleanup;
    }
    BOOL bRet=CreateProcessW(app, psArgs, nullptr, nullptr,
        FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, workDir, reinterpret_cast<LPSTARTUPINFOW>(&siex), &pi);
    ::CloseHandle(pi.hThread);
    ::CloseHandle(pi.hProcess);
Cleanup:
    DeleteProcThreadAttributeList(siex.lpAttributeList);
    DeleteAppContainerProfile(appContainerName);
    free(psArgs);
    FreeSid(sidImpl);
    return hr;
}

int wmain(int argc,wchar_t *argv[])
{
    if(argc>=2)
    {
        std::wcout<<L"Start AppContainer App: "<<argv[1]<<L"\t Return Code[HRESULT]: "<<AppContainerLauncherProcess(nullptr,argv[1],nullptr)<<std::endl;
    }
    return 0;
}

使用 Process Explorer 查看進程屬性可得到下圖: AppContainer

當我們操作時,可以看到如下結果: Open

除了 IE ,Google 的開源瀏覽器 Chromium 也在沙箱代碼中添加了 AppContainer 的支持:
http://src.chromium.org/chrome/trunk/src/sandbox/win/src/app_container.cc

Windows 的系統安全正得益於以上種種技術的出現,程序不是說想幹什麼就能幹什麼了。危險的系統操作被限制,特別是 AppContainer ,應用之間的隔離加深,跨應用的數據難以竊取。

##其他 很多開發者在 Windows 上使用沙箱來實現安全隔離,而沙箱本質上也是利用權限隔離以及 Hook 之類的技術來實現。而容器則可以在權限隔離的基礎上添加資源限制來實現,類似於作業對象限制資源,當然,如果要更加安全,隔離更加深入,必須從內核上做出努力。 開發者並不一定要專注在容器,安全上,然而一定不要濫用權限。

##備註:

  1. 用戶賬戶控制(UAC): https://en.wikipedia.org/wiki/User_Account_Control
  2. 資源管理器在開啓內置管理員的批准模式下降權是成功的,據說也是採用的計劃任務降權?

本文的 Github Pages 地址 : http://forcemz.net/container/2015/06/11/AppContainer.html

禁止未經許可的轉載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章