Пользователь
0,0
рейтинг
8 апреля 2010 в 14:35

Разработка → Перехват вызовов API-функций

C++*
— Папа, я бежал за троллейбусом и сэкономил пять копеек!
— Сынок, бежал бы за такси — сэкономил бы пять рублей!


Сегодня я хочу рассказать вам, как сэкономить 10 тысяч долларов. А заодно, что гораздо менее интересно – научить перехватывать вызовы Win32 API функций, и не только. Хотя, в первую очередь – конечно, именно их.


Основные положения


Общеизвестных методов перехвата API-функций существует ровно два, все остальные – их вариации.

Идея первого метода основана на том, что вызовы любых функций в процессе из сторонних DLL выполняется через таблицу импорта функций. Данная таблица заполняется при загрузке DLL в процесс и в ней прописываются адреса всех импортируемых функций, которые процессу могут понадобиться. Соответственно, для того, чтобы перехватить вызов API-функции, необходимо найти таблицу импорта, в ней – функцию, которую мы хотим перехватить, сохранить хранящийся там адрес (тот самый указатель на тело функции) в какую-нибудь переменную (чтобы иметь возможность самостоятельно вызвать оригинал), после чего поместить туда указатель на свою функцию. Естественно, что это надо проделать для каждого модуля (exe или dll), который находится в процессе, так как таблица импорта у каждого из них своя. Кроме того, для реализации перехвата функций, которые вызываются с использованием механизм позднего связывания (late binding), следует аналогичным образом внедриться в таблицу экспорта модуля, который эту функцию экспортирует (на сей раз, только в одну), и произвести аналогичную замену. После того, следует запретить выгрузку своей DLL на время перехвата (например, DllCanUnloadNow должна возвращать false, либо сделать лишний Lock), дабы в процессе работы dll не была выгружена, адрес перехвата не стал невалиден и вы не получили access violation со всеми вытекающими.

Данный метод, в принципе, неоднократно описывался в соответствующей литературе, а готовые реализации можно найти, например, на RSDN [1], [2]. Поэтому, мы не будем на нем останавливаться.

Гораздо более интересен второй метод – перехват функции через code injection. Идея его тоже довольно примитивна, и неоднократно описывалась. Все, что нам нужно – это затереть первые несколько байт оригинального кода функции, вставив туда инструкцию безусловного перехода на нашу функцию-перехватчик, выполнить необходимую обработку, после чего, если нам понадобится вызывать оригинальную функцию – выполнить сначала код затертого начала функции, после чего сделать безусловный переход на тело оригинальной функции, пропустив, естественно, затертое начало.

Звучит это достаточно просто, однако, для человека, всю жизнь писавшего на высокоуровневых языках, может стать неразрешимой задачей. Проблема осложняется еще и тем, что готовых реализаций данного метода не существует ввиду определенны проблем, о которых я расскажу чуть далее. Хотя… конечно, я слегка лукавлю. У Microsoft существует целый фреймворк, который посвещен решению именно этой задачи. Называется он Microsoft Detours [3], легко гуглится и стоит 10 тысяч долларов за коммерческую версию.

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

Реализация метода code injection



Начнем с самого начала. Подготовим себе небольшой тестовый стенд, на котором мы будем проверять успешность наших действий. Это будет консольный проект на C++. Для разработки я буду использовать MS Visual Studio 2010 BETA, а вы можете корректировать мои действия в зависимости от используемой IDE.

Copy Source | Copy HTML
  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3.         HANDLE hFile = CreateFile(L"d:\\test.txt", GENERIC_WRITE,  0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  4.         CloseHandle(hFile);
  5.         return  0;
  6. }


Нашей задачей будет перехват функций CreateFile и CloseHandle.

Итак, начнем с самого начала. Запустим программу на выполнение, поставив breakpoint на функцию CreateFile. Как только программа остановится, в контекстном меню нашего кода выберем пункт Go To Disassembly. И вот что мы там увидим.

HANDLE hFile = CreateFile(L"d:\\test.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
01138A8B mov esi,esp
01138A8D push 0
01138A8F push 80h
01138A94 push 2
01138A96 push 0
01138A98 push 0
01138A9A push 40000000h
01138A9F push offset string L"d:\\test.txt" (11415B0h)
01138AA4 call dword ptr [__imp__CreateFileW@28 (114527Ch)]
01138AAA cmp esi,esp
01138AAC call @ILT+1000(__RTC_CheckEsp) (11313EDh)
01138AB1 mov dword ptr [hFile],eax


Теперь, нажимая F10, дойдем до инструкции call dword ptr [__imp__CreateFileW@28 (114527Ch)] – это, собственно, и есть вызов функции, и нажмем F11. Мы попадем в тело функции CreateFile.

76D60B7D mov edi,edi
76D60B7F push ebp
76D60B80 mov ebp,esp
76D60B82 push ecx
76D60B83 push ecx
76D60B84 push dword ptr [ebp+8]
76D60B87 lea eax,[ebp-8]
76D60B8A push eax
76D60B8B call dword ptr ds:[76D11568h]


Итак, что мы тут видим?

Первая команда – mov edi, edi – ни что иное, как двухбайтовый nop (not an operation). Смысл этой команды заключается в том, чтобы сожрать один такт процессора, не сделав ничего. Ну а заодно, занять два байта в коде. Казалось бы, какая расточительность, однако же, наличие этой инструкции нам очень на руку.

Дальнейшие две команды занимают три байта и делают они следующее. Регистр esp, как известно, указывает на вершину стека, в котором хранятся все переданные в функцию через инструкцию push параметры. На вершине регистра esp (в ассемблере этот адрес записывается как [esp]) находится адрес точки возврата, который помещается туда инструкцией call (в нашем случае, это будет 0x01138AAA), а далее, вверх по стеку (стек, как известно, растет вниз) по адресу [esp + 4] находится имя файла, [esp + 8] — параметры открытия и так далее.

В стеке же хранятся локальные переменные, которые используются самой функцией. Если вы внимательно посмотрите на код, то увидите две инструкции.

76D60B82 push ecx
76D60B83 push ecx


Эти инструкции просто резервируют 8 байт в стеке, то есть оставляют место для двух переменных типа DWORD. Этот вызов трактуется именно так, потому что функция использует соглашение вызова stdcall (то есть передает параметры через стек, а не через регистры, как, например, fastcall), а регистр ecx является регистром общего назначения, и, если функция не помещала в него какого-либо значения, то в нем может лежать любой мусор, который был оставлен там предыдущим кодом. Передавать в вышестоящую функцию какие-либо мусорные данные смысла нет, поэтому мы и трактуем этот вызов именно таким образом.

Однако, после выполнения инструкции push вершина стека esp переместится на 4 байта вниз, и [esp] будет указывать уже не на адрес точки возврата, а на положенное туда только что мусорное значение. То есть мы потеряем доступ к переданным в функцию переменным! Этого допустить нельзя, а потому делается следующая вещь.

76D60B7F push ebp
76D60B80 mov ebp,esp


В стеке сохраняется текущее значение регистра базы, а в регистр базы помещается текущее начение регистра стека. Теперь мы можем адресовать переданные в функцию переменные через регистр базы (по адресу [ebp] у нас лежит сохраненное значение регистра стека, [ebp + 4] – адрес точки возврата, [ebp + 8] – имя файла, и т.д.), свободно манипулируя со стеком.

Данная пара инструкций (push ebp / mov ebp, esp) называется стандартным прологом и имет свое зеркальное отражение – стандартный эпилог, который выглядит так:

pop ebp


Однако здесь мы его не найдем – он заменен на команду leave, которая делает то же самое.

76D60BC7 leave
76D60BC8 ret 1Ch


Последняя команда – это возврат из функции с извлечением из стека 0x1c байт, что требуется по соглашению stdcall, когда функция обязана сама очищать стек после окончания работы.

Проанализировав другие API-функции, можно понять, что все они начинаются абсолютно одинаково:

mov edi,edi
push ebp
mov ebp,esp


То есть, нам в 99% случаев для «бытового» перехвата гарантировано в начале функции 5 байт, которые мы можем спокойно заменить на свой код, а потом восстановить где-нибудь в другом месте. Это хорошо, значит предельный размер нашей инструкции перехода может быть 5 байт. Этого более, чем достаточно.

Итак, теперь мы разобрались, как происходит вызов функции и готовы к ее перехвату. Осталась одна деталь – а как же собственно сделать перехват?

Для этого, все что нам нужно – это поставить в начало функции инструкцию jmp с адресом, который будет указывать на начало нашей функции. Однако, не все так просто. Дело в том, что инструкция jmp, которая бы принимала абсолютный адрес нашей функции, размером 5 байт просто не существует. Единственный jump, который работает с абсолютными адресами – это jump far, который занимает 6 байт.

Поэтому, мы будем использовать jump near, который принимает относительный адрес (то есть разность между адресом точки назначения и следующей за jump near инструкцией). По факту, для вычисления параметра операции jump near, надо из адреса точки назначения вычесть адрес исходной точки и прибавить 5 байт (именно столько эта инструкция занимает).

Copy Source | Copy HTML
  1. size_t _CalculateDispacement(void* lpFirst, void* lpSecond)
  2. {
  3.     return reinterpret_cast<char*>(lpSecond) - (reinterpret_cast<char*>(lpFirst) + 5);
  4. }


Обратившись к литературе, узнаем, что опкод функции jump near – это 0xe9. Таким образом, мы можем выполнить перехват следующим образом:

Copy Source | Copy HTML
  1. HANDLE WINAPI _My_CreateFileW(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurity, DWORD dwCreationDisp, DWORD dwFlags, HANDLE hTemplate)
  2. {
  3.     OutputDebugStringA(__FUNCTION__);
  4.     return (HANDLE)-1;
  5. }
  6.  
  7.  
  8. #pragma pack(push, 1)
  9. struct jump_near
  10. {
  11.     BYTE opcode; // 0xe9
  12.     DWORD relativeAddress;
  13. };
  14. #pragma pack(pop)
  15.  
  16. int _tmain(int argc, _TCHAR* argv[])
  17. {
  18.     HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");
  19.     jump_near* lpFunc = reinterpret_cast<jump_near*>(GetProcAddress(hKernel32, "CreateFileW"));
  20.     lpFunc->opcode = 0xe9;
  21.     lpFunc->relativeAddress = _CalculateDispacement(lpFunc, &_My_CreateFileW);
  22.  
  23.     HANDLE hFile = CreateFile(L"d:\\test.txt", GENERIC_WRITE,  0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  24.     CloseHandle(hFile);
  25.     return  0;
  26. }


Для того, чтобы устранить паразитные влияния оптимизации компилятором структур для выравнивания секций данных, мы воспользуемся дерективой #pragma pack, которая, в нашем случае, выравнивает данные по границе байта (то есть не выравнивает вовсе).

Запускаем на выполнение, и… оп, access violation. Дело в том, что страницы кода, для защиты от переполнения буфера, защищены от записи.

Однако, не все так плохо. Они защищены снаружи, а мы работаем изнутри, а потому можем обойти этот механизм, воспользовавшись функцией VirtualProtect. Поставим перед записью опкода вызов:

DWORD dwProtect = PAGE_READWRITE;
VirtualProtect(lpFunc, sizeof(jump_near), dwProtect, &dwProtect);


А после вызова:

VirtualProtect(lpFunc, sizeof(jump_near), dwProtect, &dwProtect);


Запускаем на выполнение – и, вуаля, перехват осуществлен.

Теперь, существует вторая проблема – нам нужно вызывать оригинальную функцию. Для этого, мы должны сделать следующее:
1. Сохранить указатель на начало функции.
2. Создать в памяти блок размером 10 байт с правами на выполнение кода (без них, при попытке выполнить код, мы будем получать access violation из-за реализации системы защиты NX-Bit)
3. Скопировать туда первые 5 байт исходной функции до установки туда перехватчика.
4. Создать в последних 5 байтах аналогичную инструкцию jump near, которая будет переправлять выполнение функции на оригинальный обработчик, пропуская затертые нами 5 байт.
5. Сохранить адрес 10-байтового блока и привести его к типу CreateFileWProc, который описан следующим образом:
typedef HANDLE (WINAPI *CreateFileWProc)(LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
6. Теперь, если нам нужно вызывать оригинал – мы просто пользуемся этим указателем.

Код, который реализует данный функционал в более общем случае, доступен здесь:

pastebin.com/5gZdr6Hm (заголовочный файл Detours.h)
pastebin.com/RCJ896TM (реализация Detours.cpp)

Я же, вкратце, расскажу, как это в итоге работает.

Подключим оба файла в наш проект, определим пару перехватчиков и запустим код на исполнение с breakpoint на CreateFile.

Copy Source | Copy HTML
  1. #include "Detours.h"
  2.  
  3. typedef HANDLE (WINAPI *CreateFileWProc)(LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
  4. typedef BOOL (WINAPI* CloseHandleProc)(HANDLE);
  5.  
  6. CreateFileWProc _Std_CreateFileW;
  7. CloseHandleProc _Std_CloseHandle;
  8.  
  9. HANDLE WINAPI _My_CreateFileW(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurity, DWORD dwCreationDisp, DWORD dwFlags, HANDLE hTemplate)
  10. {
  11.     OutputDebugStringA(__FUNCTION__);
  12.     return _Std_CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurity, dwCreationDisp, dwFlags, hTemplate);
  13. }
  14.  
  15. BOOL WINAPI _My_CloseHandle(HANDLE handle)
  16. {
  17.     OutputDebugStringA(__FUNCTION__);
  18.     return _Std_CloseHandle(handle);
  19. }
  20.  
  21. int _tmain(int argc, _TCHAR* argv[])
  22. {
  23.     HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");
  24.     void* lpFunc = GetProcAddress(hKernel32, "CreateFileW");
  25.     Detours::HookFunction(lpFunc, _My_CreateFileW, reinterpret_cast<void**>(&_Std_CreateFileW));
  26.     lpFunc = GetProcAddress(hKernel32, "CloseHandle");
  27.     Detours::HookFunction(lpFunc, _My_CloseHandle, reinterpret_cast<void**>(&_Std_CloseHandle));
  28.     HANDLE hFile = CreateFile(L"d:\\test.txt", GENERIC_WRITE,  0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  29.     CloseHandle(hFile);
  30.     return  0;
  31. }


По привычке, зайдем в Disassembly, дойдем до инструкции

000D8AD0 call dword ptr [__imp__CreateFileW@28 (0E527Ch)]


И нажмем F11.

Куда мы попали?

76D60B7D jmp _My_CreateFileW (0D13E8h)


Это же код перехода, установленный нашим перехватчиком. Значит, что перехват успешно выполнен!

Нажмем F10 пару раз (перескочив еще чере один промежуточный буфер, который ставит компилятор в DEBUG-версиях), и…

HANDLE WINAPI _My_CreateFileW(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurity, DWORD dwCreationDisp, DWORD dwFlags, HANDLE hTemplate)
{
000D8910 push ebp
000D8911 mov ebp,esp
000D8913 sub esp,0C0h
000D8919 push ebx
000D891A push esi
000D891B push edi
000D891C lea edi,[ebp-0C0h]
000D8922 mov ecx,30h
000D8927 mov eax,0CCCCCCCCh
000D892C rep stos dword ptr es:[edi]


Теперь, самый интересный момент. Дойдем до вызова оригинальной функции.

000D8960 call dword ptr [_Std_CreateFileW (0E4240h)]


И нажем F11.

00060000 mov edi,edi
00060002 push ebp
00060003 mov ebp,esp
00060005 jmp 76D60B82


Мы попали в так называемый трамплин – это кусок кода, который выполняет замещенные нами операции и передает управление оригинальной функции. Дойдем до jmp, нажмем F10 и увидим чудную картину.

76D60B7D jmp _My_CreateFileW (0D13E8h)
76D60B82 push ecx
76D60B83 push ecx


На сей раз мы проскочили инструкцию jmp и попали сразу на первую значимую инструкцию – push ecx. Значит, все работает так, как надо.

Потенциальные проблемы и возможности модернизации



К сожалению, код не универсален – он определяет возможность перехвата по наличию стандартного пролога WinAPI. Построить универсальное решение сложно – в начале функции в общем случае могут быть совершенно любые инструкции, включая инструкции косвенной адресации, которые при перенесении придется корректировать. Microsoft Detours решает эту проблему наличием табличного дизассемблера и корректора инструкций.

Кроме того, если размер функции меньше 5 байт, перехват просто невозможен. Такие функции иногда встречаются, но мне никогда не попадались задачи, которые требуют их перехвата. Microsoft Detours в этом случае складывает лапки.

Для создания в памяти функции-трамплина нельзя использовать операторы new и delete для работы с динамической памятью, так как память выделяется в секции данных с запретом на исполнение кода, а изменением прав на динамическую память вы открываете недоброжелателям возможность выполнить переполнение буфера. Сейчас программа работает нерационально, выделяя 4 кб памяти под каждый перехватчик – это связано с тем, что такой размер является минимальным для аллокации виртуальной памяти. По идее, нужно написать собственный менеджер памяти и использовать его. MS Detours так и поступает.

Однако, то, что я написал – вполне рабочий код, который пригодится в том случае, если ну очень надо, а денег нет. Отсутствие табличного анализатора в нем можно заменить анализатором собственным – для этого надо демопилировать требуемые функции и проанализировать их код, после чего добавить сигнатуры в _Analyze. А 4 кб памяти на перехватчик, если в программе 5-6 перехватчиков – не так много.

Использованная литература


1. Барри Брей. Микропроцессоры Intel. Архитектура, программирование и интерфейсы. Шестое издание. «БХВ-Петербург», 2005
Виктор Брыксин @bobermaniac
карма
0,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (72)

  • +4
    Я когда-то тоже писал аналогичный велосипед.

    Но вместо сравнивания первых 5 байт с фиксированной константой (в методе Analyze) я взял <a href=«www.google.ru/search?q=length+disassembler>дизассемблер длин LDE32.

    тогда каждую перехватываемую процедуру не надо описывать в _Analyze
    • 0
      p.s.: для того чтобы случайно не «задеть» косвенную адресацию, достаточно опкоды проверять с небольшим списком. т.к. в прологе большинства WinAPI функций содержатся только push и mov.
      • 0
        В большинстве API-функций в начала вообще находится стандартый 5-байтовый пролог, однако в недрах WinNT.h, например, все уже не столь радужно.
        • 0
          ntdll.dll, конечно же, а не winnt.h
        • 0
          мне этого хватало для перехвата довольно большого количества даже Nt* и Zw* функций. конечно, для нормального продукта этого тоже мало (надо перехватывать всё), но на просторах интернета есть также и бесплатные и _очень_ продвинутые аналоги detours.

          P.P.S.: в девичестве detours был бесплатен и даже не требовал detours.dll. у меня где-то даже его сорцы остались.
          • 0
            Все равно, если даже используешь аналог — неплохо было бы понимать, как в принципе он работает. Именно на это и рассчитана статья, а вовсе не на написание собственного велосипеда.
    • 0
      В данном случае, _Analyze — это просто метод-заглушка под будущую реализацию подобного механизма.

      Я именно поэтому описал в возможностях модернизации применение какого-либо дизассемблера. Именно туда его и планируется вставить.
    • 0
      Есть еще hde32, который можно использовать в этих целях. Вообще, таких решений пусть и не очень много, но они есть. Причем, по возможностям они почти не отличаются друг от друга.
  • +1
    Это всё чудесно, но какой смысл что-то перехватывать в рамках одного (своего) модуля? ;) Если же вы влезаете (inject) в другой (чужой) модуль, то появляется новая проблема, например ASLR.
    • 0
      Смысл есть. Например, в том случае, если вы — add-in для ворда. Либо — shell extension. Либо во всех остальных случаях, когда вашу DLL совершенно легально загружают в процесс.

      Кроме того, методики внедрения DLL в чужой процесс описаны на том же RSDN.
    • 0
      смотря как влезем. если боле-менее «легально» то всю эту рандомизацию нам сдаст с потрохами загрузчик.

      ASLR имеет смысл когда мы залезаем через buffer overflow и прочие «вирусные» варианты.
      • 0
        Да-да, но если мы влезаем «легально», то зачем нам что-то перехватывать? ;) Ведь тогда есть «легальные» способы для доступа к данным.
        • 0
          В том то и проблема, что они есть не всегда, и позволяют делать не все.
        • 0
          иногда требуется.
          либо, как ниже написали — слегка изменить поведение программы.
          либо, как мне однажды понадобилось, вытаскивать данные из другой программы, но не ковырянием в памяти, а по мере их поступления.
        • +1
          Нет «легальных» способов для доступа к данным если код не ваш. Например внедрение в браузер, например если вы пишете программу которая управляет другими программами те же GUI test/automation. Мои друзья когда появилась такая задача всё таки купили detours, потому что она того стоит и самим такое универсальное решение разработать будет стоить дороже.
  • 0
    «Сэкономить 10.000$» — достаточно пафосно :)

    В таком случае, используя в работе www.codeproject.com/KB/winsdk/LibMinHook.aspx, я экономлю еще и время на написание велосипеда :)
    • +3
      Там в заголовке анекдот на тему как раз для того, чтобы эту строку не воспринимали всерьез.

      За ссылку спасибо, буду изучать.
      • +1
        Я тоже нисколько не претендовал на серьёзность, просто хотел ссылкой поделиться :)

        Ах да… Совсем забыл, спасибо за статью.
  • –1
    И в который раз скажу — не надо так делать :-) Если вы, конечно, не вирье пишите :-)
    • 0
      Все зависит от поставленной задачи. Иногда приходится применять и такие методы.

      Например, через перехват CreateFile мы реализуем прозрачную загрузку файлов с сервера.
      • 0
        Не, понятно, что в некоторых случаях иначе никак и для использования нужны очень веские основания.
        • +5
          Естественно. Работа на низком уровне — большая ответственность для программиста. Это не на шарпе формочки пихать.
      • +1
        Я тоже с использованием этого механизма позволяю организовать работу в пакете NX(бывший Unigraphics) в рамках нашей plm-системы.

        Перехват функций работы с файлами+внутренних функций NX, для того, чтобы вместо имен файлов видны были обозначение+ревизия и пользователь не работал напрямую с файловой системой.
        • –3
          Не логичнее ли сделать отдельную ФС и не лезть в программу?
          • +2
            Слабо с ходу прикинуть трудозатраты на разработку?
          • +1
            Я же сказал, что вся работа ведется в рамках нашей системы.

            В данном случае, пользователь нажимает где-то в nx на кнопку требующую открытия файла с диска — перехватывается GetOpenFilename — и он видит диалоги выбора, написанные мной(думаю очевидно, что это даёт бОльшие возможности по поиску, сортировкам, выборке по критериям).

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

            Много чего удобного благодаря этому уже сделано :) Мыслите шире.
    • 0
      и если вирьё пишете, то тоже делать так не надо.
      для вирья надо (как минимум):
      1. всё-таки нормальный LDE, т.к. перехватить надо реально много функций
      2. перехват не в первых 5 байтах, а дальше (т.к. большинство антивирусов проверяют прологи на джамп за пределы модуля). вы же не хотите спалиться? ;)
      • 0
        Да не так много и надо перехватить. Зачастую recv+send хватает, чтобы пароли тырить :-)
        • 0
          ну да, для этого конечно хватит. я подразумевал user-land руткиты ;)
  • +2
    Библиотека от майкрософта:
    research.microsoft.com/en-us/projects/detours/
    • 0
      блин, читал между строк и не заметил, что у вас есть ссылка :)
      • 0
        точнее, наоборот, через строчку читал
        • 0
          Бывает, что поделать.
  • 0
    Извините, если задам сейчас глупый вопрос. Но я правильно понимаю, что программы, написанные таким образом, совершенно точно не будут работать под Wine и в ReactOS? Там ведь те же функции API реализуются совершенно иначе?

    Я не специалист в этой области, но все равно жутко интересно :)
    • 0
      Под ReactOS должно сработать, только там, вероятно, придется править метод поиска образа kernel32.dll в памяти процесса.
      • 0
        и под вайном будет.
        • +1
          если искать точки перехвата через GetModuleHandle/GetProcAddress
  • 0
    Аналогичный метод в свое время очень детально описал MsRem на wasm.ru, плюс рассмотрел варианты о том, как обмануть файерволлы, дабы они не выкидывали варнинги.
  • 0
    Интересно, что же такое создаёт автор, что требует перехвата CreateFile и CloseHandle? ;)
    • 0
      CloseHandle не нужен, он так, для примера, а вообще нужна просто прозрачная работа с серверными файлами в локальных приложениях — загрузка и выгрузка.

      Перехватчик — это только часть архитектуры, нужный только для загрузки файла по первому обращению.
  • +3
    Что-то мне подсказывает, что Detours не зря стоит 10к $.
    P.S.: боян, и метод называется splicing
  • 0
    Супербизон. Горжусь и плюсую.
  • 0
    Общеизвестных методов перехвата API-функций существует ровно два, все остальные – их вариации.

    Скажем так: первые приходящие на ум.
    Половина вопросов автора решится методом VEH (описан подробно здесь с блэкдж... с исходниками и подробными комментариями). Но у метода один минус: работает, начиная с Windows XP.
    Вообще говоря, перехват API в user-mode дело крайне неблагодарное и некрасивое.
    • 0
      Я статью еще не читал, только вкратце пробежал по заголовкам, но, честно говоря, не совсем понимаю, какое структурная обработка исключений имеет отношение к перехвату функций.
      • 0
        На самом деле, если вкратце пробежаться по заголовкам, то речь идет о векторной обработке исключений.
        Статья описывает данную технологию, и в качестве примера по ее использованию приводится в пример с перехватом функции LoadLibraryExW (к которой сводятся все UM версии LoadLibraryX). Суть в следующем: в начало функции мы вставляем одну единственную инструкцию 'INT 03', при вызове функции получаем исключение STATUS_BREAKPOINT… Ну а дальше суть понятна.
        • 0
          Да, этот способ кстати тоже вкратце задевался на RSDN. Там же описаны проблемы этого способа — какой-нибудь чужой обработчик SEH может поломать нам всю логику.

          А вероятность встретить его в общем случае ненулевая.
          • 0
            Хочу еще раз обратить Ваше внимание на то, что там затрагивается VEH.
  • 0
    Плюс тебе за анекдот про такси!
  • 0
    Спасибо за статью
  • 0
    Спасибо за статью.
    Поидее, если делать инъекцию не в свое адресное пространство, а в чужое, то, если между вызовами
    lpFunc->opcode = 0xe9;
    lpFunc->relativeAddress = _CalculateDispacement(lpFunc, &_My_CreateFileW);
    произойдет переключение задачи, и атакуемая задача попробует вызвать недоизмененную функцию, то произойдет джамп непонятно куда, что очень печально.
    Решением может быть использование одной из команд mov из SSE, которые умеют атомарно 8 или 16 байт переносить.
    • 0
      На самом деле, в реальном приложении вероятность этого довольно мала, так как подключение обработчиков происходит в момент LoadLibrary. Если хочется перестраховаться — можно использовать атомарные инструкции MMX или SSE.
      • 0
        Ну я, собственно, о перестраховке и говорю. Ситуация с переключением задачи возможна, но очень маловероятна. Но все-таки для стабильной работы программы не стоит забывать об этой проблеме. О чем я и напомнил.
  • +1
    Почему вы используете
    void* lpFunc = GetProcAddress(hKernel32, «CreateFileW»);
    вместо
    void* lpFunc = &CreateFileW;
    ?
    • 0
      Так повелось, что я для собственных функций использую амперсанд, а импортированные получаю через GetProcAddress.

      Наверное, можно и так, как вы предлагаете.
  • +1
    Кстати, про проблему, с которой можно столкнуться на x64, написано тут
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      На самом деле, он совсем не страшный — это всего-навсего один такт процессора. Любая высокоуровневая инструкция сожрет таких тактов 10-15, особенно вызов функции.

      Судя по тому, что я в свое время гуглил — она вставлена специально для возможности организовать там перехват.
      • 0
        А если вспомнить куда ведут многие API, то и 15 тактов это сущий пустяк
  • 0
    может не в тему (много букв, не прочитал все), а может кому-то пригодится — так я перехватываю waveOutWrite (к примеру):

    [pascal]

    type
    TFarJmp = packed record
    PuhsOp: byte;
    PushArg: pointer;
    RetOp: byte;
    end;

    var
    FCurrentProcess: cardinal;
    WaveOutFnAddr: pointer;
    WaveOutOldProc, WaveOutNewProc: TFarJmp;

    // init
    FCurrentProcess := GetCurrentProcess;
    WaveOutFnAddr := GetProcAddress(h, 'waveOutWrite');

    WaveOutNewProc.PuhsOp := $68;
    WaveOutNewProc.PushArg := @WaveOutWriteMod;
    WaveOutNewProc.RetOp := $C3;

    ReadProcessMemory(FCurrentProcess, WaveOutFnAddr, @WaveOutOldProc, sizeof (TFarJmp), w);
    WriteProcessMemory(FCurrentProcess, WaveOutFnAddr, @WaveOutNewProc, sizeof (TFarJmp), w);

    // fine
    WriteProcessMemory(FCurrentProcess, WaveOutFnAddr, @WaveOutOldProc, SizeOf (TFarJmp), w);

    function WaveOutWriteMod (hWaveOut: HWAVEOUT; lpWaveOutHdr: PWaveHdr; uSize: UINT): MMRESULT; stdcall;
    begin
    // чегото тут поделал скажем с данными
    WriteProcessMemory (FCurrentProcess, WaveOutFnAddr, @WaveOutOldProc, SizeOf(TFarJmp), w);
    Result := waveOutWrite(hWaveOut, lpWaveOutHdr, uSize);
    WriteProcessMemory (FCurrentProcess, WaveOutFnAddr, @WaveOutNewProc, SizeOf(TFarJmp), w);
    end;

    [/pascal]
    как-то так…
    • 0
      Ну, это, еще одна методика

      push retaddr
      ret

      Только она по размеру вроде как больше 5 байт (5 байт только на push уйдет, + байт на ret), нэ?
      • 0
        а я хз, 5 байт или сколько там… когда-то когда надо было нашел способ, проверил, что работает — и пользую, а как работает — уже «черный ящик», пусть себе работает
      • 0
        А что мешает сохранять, например, первые байт 8 или скока нада, и потом при восстановлении возвращать все на место?
        • 0
          Чем больше захватываешь кода, тем больше шанс, что
          1. Функция просто кончится
          2. Заденешь код с косвенной адресацией
  • +1
    На wasm.ru есть отличный цикл статей на тему перехвата API с примерами и библиотекой.
    wasm.ru/series.php?sid=8
    Правда, примеры на Delphi. Библиотеки для С++ можно найти на codeproject, например:
    www.codeproject.com/KB/DLL/EasyHook64.aspx
    или (писали выше) www.codeproject.com/KB/winsdk/LibMinHook.aspx
  • 0
    А как быть с DEP? B потом когда антивирусы видят VirtualProtect они начинают нервничать.
    • 0
      DEP обходится как раз VirtualProtect-ом.

      А нервничать начинают антивирусы только в том случае, если мы пишем в память чужого процесса.
      • 0
        Эээ… Я знаю, что DEP обходится VirtualProtect потому и написал про него, но меня интересует не способ разломать DEP, а способ перехвата не трогая DEP.

        Антивирусы видят, что ты можешь юзать VirtualProtect и этого уже достаточно чтобы стать подозрительным.
        • 0
          Не хотите VirtualProtect — пишите драйвер.
          • 0
            Драйвер чего?
            • 0
              Драйвер ядра для того чтобы свободно писать блоки данных в АР
  • 0
    Ужас. Я не понимаю зачем это надо в разделе С++? Какое отношение пост имеет к С++? Тоже самое можно сделать на любом другом языке, это рас. Во вторых это ужасно устарело и не знают об этом разве что дотеры и веб дизайнеры. Вот уж не думал что пост ещё и комментировать будут.
    Я разочарован хабром :(

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.