В MSDN'овской статье Process Security and Access Rights есть интересная ремарка:


… if process A has a handle to process B with PROCESS_DUP_HANDLE access, it can duplicate the pseudo handle for process B. This creates a handle that has maximum access to process B.

Если вольно перевести это на русский, то тут говорится, что имея описатель на процесс с правом доступа PROCESS_DUP_HANDLE мы можем, используя функцию DuplicateHandle(...), получить описатель с максимально разрешенными масками доступа на этот процесс.



Демонстрация


Исходный код, эксплуатирующий эту особенность, достаточно простой:


#include <Windows.h>

int wmain(int argc, PWSTR argv[])
{
    HANDLE ProcessAllAccessHandle;
    HANDLE ProcessDuplicateHandle = OpenProcess(PROCESS_DUP_HANDLE, FALSE, _wtoi(argv[1]));
    if (ProcessDuplicateHandle)
    {
        if (DuplicateHandle(ProcessDuplicateHandle, 
                            GetCurrentProcess(),
                            GetCurrentProcess(),
                            &ProcessAllAccessHandle,
                            0,
                            FALSE,
                            DUPLICATE_SAME_ACCESS))
        {
            CloseHandle(ProcessAllAccessHandle);
        }
        CloseHandle(ProcessDuplicateHandle);
    }
    return 0;
}

В результате компиляции и линковки получаем тестовую утилиту, которая в качестве аргумента принимает идентификатор целевого процесса (PID). Затем утилита открывает указанный процесс с правом PROCESS_DUP_HANDLE. Тем самым мы моделируем необходимое условие наличия описателя на процесс с правом PROCESS_DUP_HANDLE (== 0x40).


В качестве демонстрации я буду трассировать собранную утилиту в WinDbg:


0:000> lsa @$ip 0,3
>   13:     if (ProcessDuplicateHandle)
    14:     {
    15:         if (DuplicateHandle(ProcessDuplicateHandle, 
0:000> !handle @@C++(ProcessDuplicateHandle) 3
Handle 80
  Type          Process
  Attributes    0
  GrantedAccess 0x40:
         None
         DupHandle
  HandleCount   9
  PointerCount  260518

А затем легким движением руки вызовом DuplicateHandle(...) получаем второй описатель на тот же процесс, но уже с максимально широкими правами:


0:000> lsa @$ip 0,3
>   23:             CloseHandle(ProcessAllAccessHandle);
    24:         }
    25:         CloseHandle(ProcessDuplicateHandle);
0:000> !handle @@C++(ProcessAllAccessHandle) 3
Handle 84
  Type          Process
  Attributes    0
  GrantedAccess 0x1fffff:
         Delete,ReadControl,WriteDac,WriteOwner,Synch
         Terminate,CreateThread,,VMOp,VMRead,VMWrite,DupHandle,CreateProcess,SetQuota,SetInfo,QueryInfo,SetPort
  HandleCount   10
  PointerCount  292877

Ключевой момент — значение GrantedAccess, которое у нового описателя равно 0x1fffff, что соответствует PROCESS_ALL_ACCESS. К сожалению, WinDbg не выводит PID целевого процесса. Но что бы убедиться, что описатель получен на нужный процесс, можно посмотреть на описатели Process Explorer'ом (предварительно уточнив в отладчике указанный в аргументах командной строки PID):


0:000> dx argv[1]
argv[1]                 : 0x1b7c2e2412c : "21652" [Type: wchar_t *]


На скриншоте утилита открывает описатели на запущенный notepad.exe.


Почему так происходит?


Во первых потому, что при дублировании описателя, если не расширяется маска доступа к объекту (а у нас специально указан флаг операции DUPLICATE_SAME_ACCESS), не происходит проверки того, что процесс (в котором будет создан продублированный описатель) имеет доступ к этому объекту. Проверяется только то, что переданные в функцию DuplicateHandle(...) описатели процессов имеют разрешенную маску доступа PROCESS_DUP_HANDLE. А далее копирование описателя между процессами происходит без проверки прав доступа (повторюсь: если у нового описателя маска разрешенных прав не шире, чем у исходного дублируемого описателя).


А затем нужно отметить, что вызов GetCurrentProcess() возвращает константу, тот самый псевдо-описатель (pseudo handle), упомянутый в самом начале этой публикации. Существуют два документированных псевдо-описателя с константными значениями, которые физически отсутствую в таблице описателей процесса. Но эти описатели обрабатываются всеми функциями ядра (наряду с обычными описателями из таблицы описателей процесса):


Макрос Значение Описание
ZwCurrentProcess/NtCurrentProcess (HANDLE)-1 Описатель текущего процесса
ZwCurrentThread/NtCurrentThread (HANDLE)-2 Описатель текущей нити

Именно значение NtCurrentProcess (== -1) возвращает вызов GetCurrentProcess().


Этот псевдо-описатель в рамках конкретного процесса означает объект этого самого процесса с правами PROCESS_ALL_ACCESS (на самом деле есть нюансы, но статья не о них). Получается такая ссылка на самого себя, но через описатель:


То есть наш вызов DuplicateHandle(ProcessDuplicateHandle, GetCurrentProcess(), ...) будет трактован так: из открытого (целевого) процесса дублируй описатель со значением -1. А для целевого процесса (того, на который у нас сохранен описатель в переменной ProcessDuplicateHandle) значение -1 и будет ссылаться на этот самый целевой процесс с правами PROCESS_ALL_ACCESS. Поэтому в результате мы получаем описатель на целевой процесс с максимальными правами.


Вместо эпилога


Повторю мысль, написанную в самом начале: если кто-то получает на процесс описатель с правом PROCESS_DUP_HANDLE, то в рамках модели безопасности Windows он сможет получить другой описатель на этот же процесс, но с правами PROCESS_ALL_ACCESS (и сделать с процессом все, что ему заблагорассудится).


Спасибо всем, кто дочитал публикацию до конца. Приглашаю всех желающих пройти опрос, что бы узнать насколько подобные публикации могут быть интересны/полезны аудитории.

Комментарии (6)


  1. Cryptosoft
    19.04.2019 22:08

    Статья хорошая, спасибо.

    Вообще, в модели безопасности Windows имеется довольно много боковых веток и обходных путей. Но, справедливости ради, нужно отметить, что Microsoft предпринимает серьёзные усилия по их ограничению.
    Например, если раньше можно было свободно вызывать функцию SetWindowsHookEx, то теперь в Windows 10 нужно не только для этого запуститься от лица администратора, но и обладать подписанным сертификатом в составе приложения.


  1. qw1
    20.04.2019 01:18

    Вряд ли это является проблемой, потому что Windows не позволяет открыть системные процессы или процессы других пользователей с доступом PROCESS_DUP_HANDLE.

    Единственные, кому нужно помнить об этой особенности — разработчикам ядра, которые своим кодом создают новые процессы и назначают им ACL.


    1. kITerE Автор
      20.04.2019 01:27

      Более привелегированный процесс может дублировать описатель на себя (или другой привелегированный процесс) в процесс с урезанными правами, как результат какого-нибудь IPC, например.


      1. qw1
        20.04.2019 09:37

        Сложно представить такой сценарий. Привилегированный процесс даёт свой хендл для чего?

        Произвольное чтение/запись адресного пространства, создание потоков — уже нарушает безопасность.

        Разрешение чтения статистики — так можно отдать статистику вместо хендла.

        Назначение квот — это не должен делать менее привилегированный.

        Убиение процесса по запросу другого процесса — допускаю, но это плохой сценарий, т.к. у останавливаемого процесса нет шансов выполнить подчистку (удалить временные файлы, например). В этом случае лучше дать ссылку на Event, который сигналить снаружи.


        1. kITerE Автор
          20.04.2019 10:34
          +1

          Сложно представить такой сценарий. Привилегированный процесс даёт свой хендл для чего?

          В этом случае лучше дать ссылку на Event, который сигналить снаружи.

          Сложно, но, к сожалению, не невозможно. Никто не утверждал, что это best practices, но я неоднократно встречал решения, которые специально обходят механизмы безопасности Windows, например инсталляцией драйвера, что бы открывать описатели объектов с ExGetPreviousMode() == KernelMode и возвращать их в User Mode процесс.


          Пример навскидку: есть процесс, сканирующий (а возможно и закрывающий, например для безопасного извлечения флешки) открытые описатели системы. Для детализированной информации нужно получить описатель этого самого объекта. Как его можно получить, учитывая что есть процессы в соседних сессиях и более привилегированные процессы? Более привилегированным кодом, например драйвером. Но авторам кода можно не дергать драйвер на каждый описатель целевого процесса, достаточно из драйвера вернуть описатель на этот процесс с правом PROCESS_DUP_HANDLE. А затем уже процесс собственноручно сможет дублировать интересующие описатели из целевого процесса себе.


          Если рассматривать такой дырявый драйвер (а с учетом политики подписей, таскать с собой эксплуатируемый чужой подписанный драйвер уже давно не ново для вредоносного ПО), то может показаться, что поверхность атаки не велика: можно попробовать вызвать падение более привилегированного процесса, просто позакрывав ему описатели (DUPLICATE_CLOSE_SOURCE). Но, учитывая легкость и стабильность получения PROCESS_ALL_ACCESS можно запустить произвольный код в более привилегированном процессе, что (IMHO) много более опаснее.


          Менее детализированный пример навскидку #2: процесс редактирует описатель безопасности (ACL'и) у некоторого объекта процесса. С учетом представленной информации нужно осознавать, что разрешение маски доступа PROCESS_DUP_HANDLE дает полный доступ к процессу, что не очевидно.


  1. kITerE Автор
    20.04.2019 10:33

    (промахнулся)