Дата публикации статьи: 20.05.2006 17:00

Работа с описателями

Автор: Максим Павлов
[Обсудить в форуме]
WWW: http://twister-kz.narod.ru

Скачать исходный код к статье

    Описатель это манипулятор, с помощью которого Ваша программа может управлять объектами в системе. Например, открывая файл с помощью функций API, вы получаете Описатель и с его помощью можете писать в файл, читать из него и т.д. Процессы, потоки, ключи реестра, мутексы, семафоры, критические секции – всеми этими системными объектами можно управлять с помощью хэндлов. В этой статье я постараюсь рассказать, как перечислить все (ну или почти все) хэндлы объектов в системе и какую практическую пользу можно из этого извлечь. Итак, начнем…
    Программисты из Microsoft любезно предоставили в наше распоряжение функцию ZwQuerySystemInformation экспортируемую модулем Ntdll.dll. С ее помощью можно выудить кучу различной информации о системе, в том числе и получить список хэндлов, которые на данный момент присутствуют в системе. Для начала давайте разберемся с параметрами этой функции. Ее прототип на VB выглядит следующим образом:

Declare Function ZwQuerySystemInformation Lib "ntdll.dll" (ByVal infoClass As Long, _
          ByVal buf As Long, ByVal bufSize As Long, ByVal retSize As Long) As Long
  • infoClass – это константа определяющая тип информации, которую требуется извлечь. Мы будем использовать константу SYSTEM_HANDLE_INFORMATION = 2.

  • buf – это указатель на буфер в памяти, куда функция запишет нужную информацию.

  • bufSize – как не сложно догадаться, этим параметром мы указываем размер буфера в байтах. Если размер окажется слишком маленьким, то функция вернет значение STATUS_INFO_LENGTH_MISMATCH = (-1073741820).

  • retSize – сюда запишется количество реально записанных в буфер байт, но мы не будем использовать этот параметр.

Если мы все сделаем правильно, то буфер заполнится информацией следующего типа:

Type SYSTEM_HANDLE_INFORMATION_EX
	NumberOfHandles As Long
	Handles() As SYSTEM_HANDLE_INFORMATION
End Type

где NumberOfHandles – количество хэндлов в системе на данный момент, а Handles() – массив типа SYSTEM_HANDLE_INFORMATION, размер которого равен NumberOfHandles. Давайте ниже рассмотрим этот тип:

Type SYSTEM_HANDLE_INFORMATION
    ProcessId As Long
    ObjectTypeNumber As Byte
    Flags As Byte
    Handle As Integer
    Object As Long
    GrantedAccess As Long
End Type

Нам интересны не все его члены, а лишь некоторые:

  • ProcessId – идентификатор процесса-владельца данного описателя.

  • ObjectTypeNumber – номер типа описателя. Т.к. в Windows XP и Windows 2000 номера типов не совпадают, следует константы нужных типов определять динамически. Позже я покажу, как это можно реализовать.

  • Handle – собственно, сам описатель.

    Я думаю пока теории достаточно, пора попрактиковаться. Следующий код демонстрирует, как получить буфер с информацией о хэндлах. Сразу поясню – т.к. размер буфера нам заранее не известен, то мы будем выделять память под него в цикле и крутить его, пока ZwQuerySystemInformation будет возвращать STATUS_INFO_LENGTH_MISMATCH:

mSize = 4000 ‘Устанавливаем начальный размер буфера
Do
    mPtr = VirtualAlloc(0, mSize, MEM_COMMIT, PAGE_READWRITE) ‘Выделяем память
    St = ZwQuerySystemInformation(SYSTEM_HANDLE_INFORMATION, _
                        mPtr, mSize, ret) ‘Запрашиваем информацию
    If St = STATUS_INFO_LENGTH_MISMATCH Then ‘Если размер слишком мал
        VirtualFree mPtr, 0, MEM_DECOMMIT ‘Освобождаем выделенную память
        mSize = mSize * 2 ‘Удваиваем размер
    End If
Loop While St = STATUS_INFO_LENGTH_MISMATCH

    Теперь mPtr указывает на буфер с информацией о хэндлах. К сожалению, VB, в отличие от Delphi и C++ не поддерживает работу с указательными типами, поэтому нам придется немного поколдовать с помощью API-функции CopyMemory. Первые четыре байта, на которые указывает mPtr, как видно из объявления типа SYSTEM_HANDLE_INFORMATION_EX, это количество хэндлов. Сначала скопируем их в переменную hCnt, потом в цикле заполним массив Arr() значениями типа SYSTEM_HANDLE_INFORMATION. Сразу замечу, что длина этого типа равняется 16 байтам. Итак, все выше перечисленное делает следующий код:

CopyMemory ByVal VarPtr(hCnt), ByVal mPtr, 4
Dim Arr() As SYSTEM_HANDLE_INFORMATION
ReDim Arr(1 To hCnt)
mPtr2 = mPtr
mPtr = mPtr + 4
For i = 1 To hCnt
    CopyMemory ByVal VarPtr(Arr(i)), ByVal mPtr, 16
    mPtr = mPtr + 16
Next
‘Не забудем освободить выделенную память
VirtualFree mPtr2, 0, MEM_DECOMMIT 

    Теперь у нас готов массив с информацией. Что дальше? Дальше научимся по хэндлу получать название его типа. Зачем? Повторюсь, что номера типов для WinXP и Win2000 могут различаться, а названия нет… Правда тут не все так просто. Прежде чем мы сможем оперировать чужим хэндлом, мы должны его скопировать к себе. А чтобы иметь возможность копировать любые хэндлы мы должны добавить к своему процессу соответствующие привилегии. Я не буду на этом подробно останавливаться, т.к. добавление привилегий выходит за рамки данной статьи, тем более что в программе, которую я прилагаю к статье, все это есть. Итак. Открываем процесс-владелец описателя с доступом PROCESS_DUP_HANDLE и копируем описатель к себе:

hProcess = OpenProcess(PROCESS_DUP_HANDLE, 0, Arr(i).ProcessId)
DuplicateHandle hProcess, Arr(i).Handle, GetCurrentProcess, _
			hHandle, 0, 0, DUPLICATE_SAME_ACCESS

    После этого в переменной hHandle содержится скопированный описатель. Теперь можно узнать название типа. Для этого следует воспользоваться функцией ZwQueryObject, которую так же экспортирует модуль Ntdll.dll. Вот ее прототип:

Declare Function ZwQueryObject Lib "ntdll.dll" (ByVal ObjectHandle As Long, _
     ByVal ObjectInformationClass As Long, ByVal ObjectInformation As Long, _
    ByVal ObjectInformationLength As Long, ByVal ReturnLength As Long) As Long
  • ObjectHandle – это описатель, информацию о котором мы будем получать.

  • ObjectInformationClass – константа, определяющая тип извлекаемой информации. Для получения имени типа объекта следует использовать значение OBJECT_TYPE_INFORMATION_CLASS = 2, а для получения названия объекта OBJECT_NAME_INFORMATION_CLASS = 1.

  • ObjectInformation – это указатель на буфер.

  • ObjectInformationLength – это длина буфера.

  • ReturnLength – это количество реально скопированных в буфер данных.

Указав в ObjectInformationClass значение OBJECT_TYPE_INFORMATION_CLASS, в буфере мы получим данные типа OBJECT_TYPE_INFORMATION:

Public Type OBJECT_TYPE_INFORMATION
    Name As UNICODE_STRING
    ObjectCount As Long
    HandleCount As Long
    Reserved1(4) As Long
    PeakObjectCount As Long
    PeakHandleCount As Long
    Reserved2(4) As Long
    InvalidAttributes As Long
    GenericMapping(4) As Long
    ValidAccess As Long
    Unknown As Byte
    MaintainHandleDatabase As Boolean
    PoolType As Byte
    PagedPoolUsage As Long
    NonPagedPoolUsage As Long
End Type

В данном случае нас интересует только переменная Name типа UNICODE_STRING:

Type UNICODE_STRING
    Length As Integer
    MaximumLength As Integer
    Buffer As Long
End Type

    Переменная Buffer будет указывать на строку типа WideChar. Чтобы из этой переменной получить обычную VB-строку следует воспользоваться API-функцией SysAllocString с последующей конвертацией результата через StrConv .
Итак, получаем название типа:

Dim oti As OBJECT_TYPE_INFORMATION
Dim vb_str As String
mPtr = VirtualAlloc(0, &H2000, MEM_COMMIT, PAGE_READWRITE)
ZwQueryObject hHandle, OBJECT_TYPE_INFORMATION_CLASS, _
			ByVal mPtr, &H2000, 0
CopyMemory ByVal VarPtr(oti), ByVal mPtr, 16
vb_str = StrConv(SysAllocString(oti.Name.Buffer), vbFromUnicode)
VirtualFree mPtr, 0, MEM_DECOMMIT

    Переменная vb_str будет содержать строку с результатом. Теперь еще один важный момент – получение имени объекта. В принципе, все не так уж сложно – следует воспользоваться все той же ZwQueryObject, указав в ObjectInformationClass значение 1 и в буфере «поймать» переменную типа UNICODE_STRING, которая и будет содержать указатель на имя объекта. Но все гладко лишь в теории. На самом деле придется столкнуться с одной серьезной проблемой. Дело в том, что, запрашивая информацию о хэндле открытого именованного канала (ZwQueryObject или ZwQueryInformationFile, не важно), работающего в блокирующем режиме, вызывающий поток будет ждать поступления сообщения в канал. А ведь это событие может и не произойти. Из-за этого поток «умирает». Самое ужасное что такой «мертвый» поток уже невозможно никак закрыть, а процесс, имеющий такие потоки выгружается из памяти только по ресету… Но выход, конечно, есть! Нам нужно в отдельном потоке запустить процедуру, которая с помощью функции GetFileType попытается определить тип файла (если конечно мы опрашиваем описатель типа File, иначе, в принципе, вызов GetFileType не имеет смысла). Как ни странно, но вызывающий поток все же повиснет, но его все-таки можно будет завершить с помощью функции TerminateThread и принудительной очистки стека потока. Еще эта проблема решается с помощью драйвера, но так на VB драйвер не напишешь, мы этот вариант рассматривать не будем. Итак, процедура, опрашивающая описатель и запускаемая нами в отдельном потоке (не забудьте поместить ее в отдельном модуле) выглядит так:

Public GlobalHandle As Long, GlobalThreadStack As Long
Public Sub TestFileThread()
  Dim mbi As MEMORY_BASIC_INFORMATION
  VirtualQuery ByVal VarPtr(mbi), mbi, Len(mbi)
  GlobalThreadStack = mbi.AllocationBase
  GetFileType GlobalHandle
End Sub

А этот код будет создавать поток, ждать его завершения в течение 20 мс. и в случае его зависания прибивать. Соответственно, если поток завис, мы не будем вызывать ZwQueryObject:

    Dim oni As UNICODE_STRING
    bQueryObject = True
    GlobalHandle = hHandle
    hThread = CreateThread(ByVal 0&, ByVal 0&, _
		AddressOf TestFileThread, ByVal 0&, ByVal 0&, hThreadID)
    If WaitForSingleObject(hThread, 20) = STATUS_TIMEOUT Then
        TerminateThread hThread, 0
        ‘GlobalThreadStack указывает на стек потока
  If GlobalThreadStack <> 0 Then VirtualFree GlobalThreadStack, 0, MEM_RELEASE
        bQueryObject = False
    End If
    CloseHandle hThread
    If bQueryObject Then
        mPtr = VirtualAlloc(0, &H2000, MEM_COMMIT, PAGE_READWRITE)
        ZwQueryObject hHandle, 1, ByVal mPtr, &H2000, 0
        CopyMemory ByVal VarPtr(oni), ByVal mPtr, Len(oni)
        sVBStr = GetStrFromPtrW(oni.Buffer)
        VirtualFree mPtr, 0, MEM_DECOMMIT
    End If
    

    Итак, мы научились перечислять хэндлы, получать кое-какую информацию о них. Теперь стоит поговорить о практическом применении данной методики. Что же можно сделать, имея за плечами эти навыки? На самом деле много чего, все зависит от Вашего воображения и знаний. Я приведу несколько примеров:

  1. Поиск скрытых процессов в юзермоде. Обычно скрытие процессов в юзермоде осуществляется путем перехвата ZwQuerySystemInformation, но многие просто забывают скрыть хэндлы, связанные с процессом (хотя скрытие хэндлов ни чуть не сложнее, чем скрытие самого процесса). Нам нужно лишь перечислить хэндлы и на основании полученной информации получить список процессов. Код получается не сложным, если учесть что хэндлы в буфере упорядочены по PID-ам.

  2. «Убийство» антивируса Касперского. KAV устанавливает в системе драйвер, который перехватывает ZwOpenProcess, ZwTerminateProcess и ZwTerminateThread, после чего запрещает любую работу этих API с своим процессом. Именно поэтому мы не можем получить описатель KAV-а. Но! Сервер подсистемы csrss.exe имеет хэндлы всех процессов и потоков в системе. Нам нужно лишь найти нужный описатель (ProcessId в структуре SYSTEM_HANDLE_INFORMATION нужно сравнивать с PID-ом csrss.exe и если они равны, то опрашивать описатель с помощью ZwQueryInformationProcess. Если в полученном буфере (тип PROCESS_BASIC_INFORMATION) поле UniqueProcessId равно PID-у KAV, то значит нашли), скопировать его к себе и завершить процесс KAV-а (под отладкой).

  3. Нахождение процесса, которым занят файл. Иногда требуется найти процесс(ы) которые работают с каким-либо файлом, например, когда файл невозможно удалить. Все просто – необходимо перечислить хэндлы файлов, опросить их с помощью ZwQueryInformationFile и сравнить имя файла с нужным.

  4. Работа с занятыми файлами. Иногда файл невозможно даже прочитать, т.к процесс, работающий с этим файлом запретил его чтение. Тоже не проблема - если файл открыт каким-либо процессом, то этот процесс имеет его описатель. Нужно лишь скопировать нужный описатель к себе. Правда тут имеются подводные камни: после копирования оба описателя (наш и процесса открывшего файл) будут указывать на один FileObject, следовательно, текущий режим ввода-вывода, позиция в файле и другая связанная с файлом информация будут общими у двух процессов. Поэтому чтение файла будет вызывать изменение позиции чтения и нарушение нормальной работы программы открывшей файл. Чтобы этого избежать, нам нужно останавливать потоки процесса владельца файла (ZwSuspendProcess или ZwSuspendThread), сохранять текущую позицию, копировать файл, восстанавливать текущую позицию и запускать процесс владелец снова. К сожалению, этот метод не всегда приемлем – например, скопировать файлы реестра на работающей системе с его помощью не удастся.


    Примечания
    1. Спасибо tyomitch и GSerg за разъяснение некоторых моментов, связанных с оптимизацией конвертации строк.
    2. Многопоточность в Visual Basic не так легко реализовывается, как кажется на первый взгляд. Подробнее об этом можно почитать в этом сообщении на форуме.