В этой статье я хочу поделиться практическими методами отладки, модификации и устранения багов в 32-х и 64-х разрядных приложениях под ОС Windows, разработанных на языке C/C++, исходные коды которых по тем или иным причинам не стали достоянием общественности.

Этот пробел отчасти можно попытаться устранить, например, с помощью плагина Hex-Rays для IDA Pro, и зачастую удаётся довольно качественно восстановить нужный участок исходного кода, обнаружив в нём проблемное место. Но после этого всегда возникает вопрос - что с этим исправленным кодом делать дальше, как и где его можно использовать? На данном этапе мне всегда хотелось взять этот отдельно декомпилированный фрагмент программы, поменять в нём что-нибудь и затем каким-то чудесным образом «поместить обратно» в программу.

Далее будет описан один из возможных способов реализации этой идеи на практике.

Иногда возникает необходимость исправить какой-нибудь баг в сторонней программе, добавить к ней отсутствующий функционал, изменить существующий, либо модифицировать логику некоторых процессов, особенно если нет возможности обратиться за помощью к разработчикам. Описание процедуры выявления «интересных» мест выходит за рамки данной статьи. В некоторых случаях это может быть банальная ошибка в программе, которую удаётся вылечить несложным патчем непосредственно в теле EXE или DLL файла. Но если же предстоит исправлять алгоритмы поведения некоторых функций, либо добавлять новые, обычным «байтовым» патчем здесь уже не обойтись. Устранение же некоторых на первый взгляд простых багов может в итоге вылиться в «перепахивание» доброй половины программы. Также приходится потрудиться, чтобы заставить правильно работать приложения, созданные, например, под Win XP в новых ОС Windows.

Освоив и давно применяя на практике некоторые методы модификации программного кода, самым удобным и простым на мой взгляд способом оказалась его модификация в ран-тайме с помощь метода DLL-инъекции. На эту тему существует огромное количество материала и в плане реализации самого метода DLL-инъекции данная статья не претендует ни на новизну, ни на оригинальность, но в большей степени призвана систематизировать практические наработки в области отладки, модификации, поиска и устранения багов в чужом коде с его помощью.

Описываемый здесь способ вынесения некоторых функций сторонних приложений в свою собственную программу на C/C++ позволяет отлаживать, модифицировать и при необходимости более детально изучать их работу с помощью отладчика на высокоуровневом языке, вместо того, чтобы «дебажить» их в дизассемблированном коде.

Прилагаемый к данной статье пример с помощью минимальных изменений можно использовать в своих целях в качестве готового проекта для Visual Studio (VS).

В VS при создании 32-х разрядных приложений платформа называется «x86», а 64-x разрядных «x64». Поэтому чтобы не путаться с названиями я буду для обозначения разрядности приложений использовать термины «Win32» и «Win64».

Инструментарий.

  • IDA Pro (файлы примеров в проекте для v7.5).

  • CFF Explorer или любой другой инструмент редактирования импорта.

  • Visual Studio С/C++ (проект примера для Platform Toolset: Visual Studio 2022 (v143)).

Практика внедрения.

Для примера рассмотрим тестовую программу SimpleCalc – простое оконное приложение (проект для VS прилагается), которое производит сложение и умножение двух чисел.
В программу намеренно внесён «баг», приводящий к тому, что для операций «2+2» и «2*2» результат получается равным 5. Заголовок диалогового окна данного приложения - «Simple Buggy Calc ( 2+2=5; 2*2=5 )».

Следовательно, нам будет необходимо модифицировать его работу таким образом, чтобы в результате выполнения уникальных операций 2+2 и 2*2 получались правильные значения, а текст заголовка окна выглядел бы, как «Simple Calc».

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

Для начала в дизассемблированном коде программы необходимо найти методы, выполняющие операции сложения и умножения, а также место создания главного окна. Воспользуемся для этого дизассемблером IDA Pro.

Займёмся описанием класса Adder (имя этому классу можно задать любое).

Для работы с классами в IDA Pro существует плагин ClassInformer. В некоторых случаях он может помочь восстановить иерархию классов и имена виртуальных таблиц, чтобы получить близкие к оригиналу имена классов.

Место создания класса Adder обнаруживается в методе DialogProc, откуда можно определить его размер (для Win32 версии):

INT_PTR CALLBACK DialogProc( HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
  ...

  pAdder = operator new( 0x10 );          // Размер структуры для класса Adder
  if( pAdder )
  {
    sub_401870( pAdder, 0x10 );           // Метод инициализации, созданный компилятором
    pAdder = sub_401160( pAdder, hDlg );  // Вызов конструктора Adder::Adder()
  }
  else
  {
    pAdder = 0;
  }
  _pAdder = pAdder;

  ...

Псевдокод конструктора в IDA Pro:

void* __thiscall sub_401160( void* this, HWND hDlg )
{
  *(_DWORD*)this = &Adder::`vftable';
  *((_DWORD*)this + 2) = GetDlgItem( hDlg, 1001 );
  *((_DWORD*)this + 3) = GetDlgItem( hDlg, 1002 );
  *((_DWORD*)this + 1) = GetDlgItem( hDlg, 1003 );
  return this;
}

Теперь создим в IDA Pro структуру такого же размера с именем класса. В нашем случае структура для класса и интерфейса будет выглядеть так:

IAdder          struc ; (sizeof=0x4)
  Add             dd ?      ; Адрес метода, выполняющего операцию сложения.
IAdder          ends

Adder           struc ; (sizeof=0x10)
  pVftable        dd ?      ; IAdder* - указатель на таблицу методов интерфейса IAdder
  _hRes           dd ?      ; HWND - идентификатор окна суммы
  _hAdd1          dd ?      ; HWND - идентификатор окна ввода для первого слагаемого
  _hAdd2          dd ?      ; HWND - идентификатор окна ввода для второго слагаемого
Adder           ends

После присваивания типа вновь созданной структуры Adder соответствующим указателям и переименования методов класса, процедура его создания преобразится следующим образом:

INT_PTR CALLBACK DialogProc( HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
  ...
  pAdder = (Adder*)operator new( sizeof( Adder ) );
  if( pAdder )
  {
    Adder:: __autoclassinit2( pAdder, sizeof( Adder ) );  // Метод инициализации, созданный компилятором
    pAdder = Adder::Adder( pAdder, hDlg );                // Конструктор
  }
  else
  {
    pAdder = 0;
  }
  _pAdder = pAdder;
  
  ...

 А конструктор будет теперь выглядеть так:

Adder* __thiscall Adder::Adder( Adder* this, HWND hDlg )
{
  this->pVftable = (IAdder*)&Adder::`vftable';      // Адрес таблицы методов интерфейса IAdder
  this->_hAdd1 = GetDlgItem( hDlg, IDC_ADD_EDIT1 );
  this->_hAdd2 = GetDlgItem( hDlg, IDC_ADD_EDIT2 );
  this->_hRes = GetDlgItem( hDlg, IDC_ADD_RES );
  return this;
}

Т.к. вызов реализованного метода Add интерфейса IAdder в программе происходит через таблицу адресов, найдём его в этой таблице Adder::`vftable'.

После причёсывания декомпилированного пвсевдокода в IDA Pro метод Adder::Add будет выглядеть примерно так:

// 0x4011B0
void __thiscall Adder::Add( Adder* this )
{
  int res;
  int add2;
  int add1;
  char* pBuf;

  pBuf = (char*)operator_new( 64 );

  GetWindowTextA( this->_hAdd1, pBuf, 64 );
  add1 = itol( pBuf );

  GetWindowTextA( this->_hAdd2, pBuf, 64 );
  add2 = itol( pBuf );

  // Этот «баг» нам необходимо будет исправить в своей DLL.
  //
  if( add1 == 2 && add2 == 2 )
    res = 5;
  else
    res = add2 + add1;

  sprintf_s( pBuf, 0x40, "%ld", res );
  SetWindowTextA( this->_hRes, pBuf );

  DeleteBuf( this, pBuf )
}

 Здесь для нас важно запомнить адрес этого метода - 0x4011B0.

Теперь перейдём к поиску и декомпиляции метода, производящего операцию умножения.
Его вызов также обнаруживается в методе DialogProc и после декомпиляции (для Win32 версии) будет выглядеть так:

// 0x401150
void __usercall Mult( HWND hMul1@<eax>, HWND hMul2@<ebx> )
{
  Mult_( hMul1, hMul2 );
}

 А код метода Mult_() так:

void __stdcall Mult_( HWND* hMul1, HWND* hMul2 )
{
  int res;
  int mul2;
  int mul1;
  char buf[64];

  GetWindowTextA( hMul1, buf, sizeof( buf ) );
  mul1 = itol( buf );

  GetWindowTextA( hMul2, buf, sizeof( buf ) );
  mul2 = itol( buf );

  // Этот «баг» нам необходимо будет исправить в своей DLL.
  //
  if( mul1 == 2 && mul2 == 2 )
    res = 5;
  else
    res = mul2 * mul1;

  sprintf_s( buf, sizeof( buf ), "%ld", res );

  SetWindowTextA( g_hMulRes, buf );
}

В программе SimpleCalc метод Mult() намеренно был создан, как псевдо __usercall с передачей параметров через регистры EAX и EBX. Компилятор от MS для Win32 использует соглашения о вызовах __cdecl,  __stdcall или __thiscall, но некоторые другие компиляторы могут использовать иные соглашения и примером такого поведения является метод Mult().

Конечно, в данном конкретном случае можно было бы ограничиться исправлением только __stdcall метода Mult_(), но для полноты картины опишем способ с нестандартным для MS соглашением о вызовах.

Для того, чтобы поместить значения регистров в стек для вызова «обычного» метода __stdcall  Mult_() из метода Mult(), воспользуемся атрибутом naked. Этот атрибут указывает компилятору не создавать пролог и эпилог внутри метода, чтобы не испортить значения регистров EAX и EBX (в нашем случае) до помещения их в стек:

// void __usercall Mult( HWND hMul1@<eax>, HWND hMul2@<ebx> )
__declspec( naked )
void Mult()
{
  __asm
  {
    push	ebx
    push	eax
    call	Mult_

    ret
  }
}
PATCH( 0x401150, Mult )

Можно было бы написать код метода Mult() и на чистом ассемблере, а для Win64 это единственный способ, так как в MSVC для него не поддерживается атрибут naked. Но для Win64, как правило, используется соглашение о вызове __fastcall и другие, по крайней мере среди тех приложений, которые мне приходилось «лечить», не попадались.

В методе Mult_(), реализующем операцию умножения, мы обнаруживаем использование глобальной переменной:

// 0x41B36C
HWND g_hMulRes;

Адреса метода Mult() и глобальной переменной g_hMulRes нам понадобятся, поэтому их необходимо будет запомнить.

Способ изменения заголовка диалогового окна будет описан чуть позже.

Создание инжектируемой DLL.

Итак, мы обнаружили и декомпилировали интересующие нас методы и теперь перейдём к созданию собственной DLL, которую позже внедрим в тестовую программу SimpleCalc. Проект такой DLL (Win32 и Win64) для VS прилагается.

Основная идея метода DLL-Injection заключается в том, что на этапе загрузки приложения, но ещё до передачи ему управления, загрузчик ОС, среди прочих, загрузит в общее адресное пространство и нашу DLL (которую мы добавим в секцию импорта EXE файла) и вызовет из неё метод:

BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason, LPVOID lpReserved )

со значением переменной ul_reason равным DLL_PROCESS_ATTACH.

Для упрощения описания не будем рассматривать здесь частные случаи типа отложенной загрузки, LoadLibrary или TLS-коллбэков.

На данном этапе мы должны модифицировать начальную часть перехватываемых нами методов из EXE файла по адресам, полученным из IDA Pro, разместив там команды безусловных переходов на адреса наших переписанных в DLL методов.

Для Win32 команда перехода, содержащая адрес непосредственно в своём операнде (а нам нужна именно такая, чтобы не портить ни стек, ни регистры) состоит из пяти байт, первый - 0xE9, а остальные четыре знаковое смещение относительно адреса следующей команды.
Для Win64 эта команда состоит из шести байт: 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 и следом за ними восемь байт адрес перехода – итого четырнадцать байт.

Здесь необходимо следить за тем, чтобы длина команды перехода была не больше длины самого модифицируемого метода! Как, например, для «заглушек», содержащих единственную однобайтовую команду ret.

Установкой адреса перехода занимается метод Patch(). Так как адресное пространство EXE модуля, в котором расположен исполняемый код недоступно для записи, сначала мы модифицируем права доступа к этой памяти с помощью функции VirtualProtect, затем записываем в начало перехватываемого метода нужные байты – команду перехода на наш метод в DLL и в конце восстанавливаем прежние права с помощью той же функции VirtualProtect.

При расчёте адреса перехода существует одна тонкость, заключающаяся в том, что IDA Pro по умолчанию дизассемблирует EXE файл по стандартному адресу загрузки - 0x00400000 (либо 0x140000000 для Win64). Но иногда встречаются EXE файлы, у которых в заголовке в параметре IMAGE_NT_HEADER.OptionalHeader.DllCharacteristics установлен флаг «DLL Can Move» и в этом случае ОС вправе загрузить его по любому другому адресу.

В свойствах проекта SimpleCalc в VS – за этот флаг отвечает пункт «Linker -> Advanced -> Randomize Base Address», который намеренно установлен в «Yes» для тестирования данного поведения.

Следовательно, в нашей DLL мы должны учитывать адрес, по которому загружен EXE файл, и затем использовать его в методе Patch() для расчёта смещения в команде перехода относительно адреса взятого из IDA Pro.

При работе с большими проектами, в которых используется множество классов, исходный код DLL в VS обрастает большим количеством отдельных файлов. И следить за тем, перехвачен ли тот или иной метод в DllMain с помощью вызова Patch() становится неудобным. Поэтому впоследствии был использован способ для объявления перехвата непосредственно возле нужного метода с помощью вспомогательной статической структуры. При этом оказалось возможным отказаться от использования DllMain, как места для объявления всех перехватов.

В дополнение к этому был создан вспомогательный класс CPatch.

При загрузке DLL ещё до попадания в точку входа (DllMain) инициализируются все статические объекты и таким образом, через вызов метода Patch( exeAddr, dllAdd ) происходит замена первых байт из оригинального метода в EXE на команду перехода в переписанный нами метод в DLL.

Класс CPatch содержит в себе контейнер std::map<ADDR, PATCH_DATA>, ключом в котором является адрес оригинального метода из EXE, а значением – структура PATCH_DATA, в которой хранятся оригинальные байты, заменённые на команду перехода и байты самой команды перехода. При каждом вызове метода Patch( exeAddr, dllAddr ) в этот контейнер добавляется новый элемент с соответствующим ключом – адресом оригинального метода из EXE и структурой PATCH_DATA, описанной выше.

В классе CPatch также содержатся методы Patch( exeAddr ) и Unpatch( exeAddr ), смысл использования которых состоит в следующем. Иногда бывает необходимо «попасть» в какой-нибудь метод из EXE, чтобы проконтролировать передаваемые в него аргументы, сохранив их значения, скажем, в лог файл, но весь метод для этого переписывать и декомпилировать в наши планы пока не входит.

Приведём пример использования этих методов для изменения заголовка диалогового окна программы SimpleCalc.

«Пропатчим» оконную процедуру диалогового окна DialogProc таким образом, что при попадании в наш переписанный метод DialogProc в DLL мы каждый раз будем восстанавливать оригинальные байты в EXE методе с помощью _patch->Unpatch(), вызывать оригинальный метод и после этого проверять значение аргумента uMsg. Если оно будет равным WM_INITDIALOG, для нас это может означает, что можем менять его заголовок. При всех других значениях аргумента uMsg, перед выходом из нашего метода DialogProc мы снова будем меняем начальные байты оригинального метода EXE на команду перехода на наш метод в DLL с помощью _patch->Patch().

// 0x4012C0
INT_PTR CALLBACK DialogProc( HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
  _patch->Unpatch( 0x4012C0 );

  auto res = ( ( INT_PTR( CALLBACK* )( HWND, UINT, WPARAM, LPARAM ) )A( 0x4012C0 ) )( hDlg, uMsg, wParam, lParam );

  if( uMsg == WM_INITDIALOG )
  {
    ::SetWindowText( hDlg, "Simple Calc" );
  }
  else
  {
    _patch->Patch( 0x4012C0 );
  }

  return res;
}
PATCH( 0x4012C0, DialogProc )

Проверка работоспособности инжектируемой DLL.

После создания DLL, в которой описан класс Adder, метод Mul() и DialogProc() необходимо дополнительно объявить в ней один экспортируемый метод с любым именем. В нашем случае им будет пустой метод Setup(). В свойствах проекта в VS помещаем созданную DLL в ту же директорию, что и EXE файл.

Теперь настало время добавить в секцию импорта программы SimpleCalc созданную нами DLL и единственный экспортируемый из неё метод Setup(). Для этого можно воспользоваться утилитой CFF Explorer. Открываем в ней EXE файл SimpleCalc, в левой части окна выбираем пункт «Import Adder», в открывшемся окне нажимаем кнопку «Add» и выбираем наш DLL файл. После этого в окне «Exported Functions» должен отобразиться наш единственный экспортируемый метод Setup(), выделяем его мышкой и нажимаем кнопку «Import By Name» после чего он отобразится в соседнем справа окне «Imported Functions»:

Затем нажимаем кнопку «Rebuild Import Table» и перезаписываем исходный EXE файл через меню "File -> Save" или сохраняем его с новым именем "File -> Save As".

Часто возникает соблазн внедрить свой код в чужой файл без какой бы то нибыло его модификации. Обычно любой EXE файл импортирует несколько системных DLL из которых, к тому же, используются далеко не все методы. При этом можно было бы создать свою DLL с тем же именем, скажем, kernel32.dll. Описать в ней прокси переходы для всех импортируемых из этой DLL методов, добавить свои и в итоге поместить её в тот же каталог рядом с EXE файлом. Но MS об этом тоже подумала и создала список так называемых Known DLLs, куда входят все системные, чтобы они не могли загружаться из того же каталога, что и EXE.

Существует способ обойти это ограничение, но в мои планы не входило создание вирусов, программ-шпионов, или подключение к уже запущенным процессам, поэтому модификация секции импорта EXE файла выглядит в данном случае абсолютно приемлемой и совершенно несложной операцией.

Теперь в VS в свойствах DLL проекта в разделе «Debugging» в пункте «Command» прописываем путь к модифицированному EXE файлу, ставим точки останова на начало методов Add, Mult, DialogProc и стартуем отладку. Сначала мы должны попасть в метод DialogProc, а при нажатии в окне программы на кнопку «=» - в соответствующий метод вычисления суммы или произведения. Убеждаемся, что патч программы был произведен успешно.

Вызов методов EXE файла из DLL.

Как правило, из метода, который мы переписали в своей DLL, вызывается несколько других, которые на данном этапе нас не интересуют, либо мы планируем перейти к их разбору позже.

Предположим, у нас есть метод sub_4037B0, из которого вызывается другой, по адресу 0x404800. Тогда, определив из дизассемблера соглашение о вызове этого метода и тип аргументов, можно будет вызвать его таким образом:

int sub_4037B0()
{
  HWND hDlg;

  ...

  // int __cdecl sub_404800( HWND hDlg );
  int res = ((int(__cdecl*)(HWND))0x404800)( hDlg );

  ...
}

Если же он вызывается из множества мест нашей DLL, но декомпилировать его пока в наши планы не входит, можно реализовать его таким образом:

int __cdecl sub_404800( HWND hDlg )
{
  return ((int(__cdecl*)(HWND))0x404800)( hDlg );
}

Для случая, если метод является членом класса, его вызов будет выглядеть так:

int ClassA::sub_4037B0()
{
  HWND hDlg;

  ...

  // int __thiscall ClassA::sub_404800( HWND hDlg );
  int res = ((int(__thiscall*)( ClassA*, HWND ))0x404800)( this, hDlg );

  ...
}

или, опять же:

int ClassA::sub_404800(( HWND hDlg )
{
  return ((int(__thiscall*)(ClassA*, HWND))0x404800)( this, hDlg );
}

Способ вызова методов с нестандартными для MSVC соглашениями о вызовах был описан выше на примере с __usercall Mult().

Доступ к данным EXE файла из DLL.

В случае, если метод сторонней программы, перехват которого производится, был написан с использованием классов и все данные, используемые в этом методе – члены этого (или другого) класса, то доступ к ним в своём переписанном методе сводится лишь к правильному описанию самого класса. Если все члены класса расположены в его описании строго на своих местах, то и доступ к ним будет осуществляться автоматически по их имени (как в классе Adder). Их значения также будут отображаться в отладчике VS через указатель this.
Но в случае использования глобальных переменных ситуация усложняется. Доступ к таким переменным можно получить лишь по их адресу в памяти основной программы, взятому из дизассемблера. В MSVC, к сожалению, невозможно объявить переменную по абсолютному адресу (даже без выделения для неё памяти), как это делается во многих Embedded системах, поэтому приходится объявлять глобальные переменные для указателей, например, таким образом:

#define _ptr1 ((int*)0x40BEF4)

а для переменных, используемых по значению, таким:

#define _val1 (*(int*)0x40BEF8)

Но, во-первых, при этом возникает огромное неудобство при отладке программы. Дело в том, что VS не отображает в отладчике значения макросов, объявленных с помощью директивы препроцессора #define. А во-вторых, что ещё более печально, для указателя, объявленного таким образом, не удаётся получить его адрес:

int** pptr1 = &_ptr1; // error C2101: '&' on constant

Можно объявить и инициализировать указатель так:

int* _ptr1 = (int*)0x40BEF4;

И это будет работать для отображения его значения в отладчике и для операции разадресации «*_ptr1», но как только мы попытаемся получить адрес этого указателя, им окажется адрес нашей локальной переменной _ptr1 в адресном пространстве DLL.

Применение оператора «placement new» тоже не даёт нужного результата, т.к. непосредственно переменную с нужным именем разместить по конкретному адресу не удаётся. Так, например, конструкция типа:

int* _ptr1 = new((int*)0x40BEF4) int(5);

действительно размещает объект типа int со значением 5 по адресу 0x40BEF4, но не переменную _ptr1 ! А она опять же находится в адресном пространстве DLL, что для нашей задачи равносильно определению:

int* _ptr1 = (int*)0x40BEF4;

Поэтому в качестве универсального метода пришлось прибегнуть к такой страшноватой конструкции, помещая переменную в структуру:

#define VAR(name,type,addr) \
  typedef struct { type var; } name##_; \
  auto name = (name##_*)(addr);

Здесь мы сначала объявляем новый тип структуры, который содержит в себе единственный элемент – нашу переменную с именем var. Затем создаём указатель на эту структуру с именем нашей переменной и инициализируем его адресом этой переменной из EXE файла.

Объявление глобальной переменной g_hMulRes в этом случае будет выглядеть так:

VAR( g_hMulRes, HWND, 0x41B36C )

 а обращение к ней так:

g_hMulRes->var

Несколько «ugly», но зато с этой переменной можно производить операции взятия адреса, разадресации и, что немаловажно, её значение будет отображаться в отладчике.

Буду весьма признателен, если кто-нибудь подсказажет более изящный способ решения этого вопроса.

Управление памятью.

При сборке EXE файла использовалась та или иная версия библиотеки C/C++ (например, CRT), в которой были определены операторы и методы для управления памятью «new», «delete», «malloc», «free» и другие. И эта версия может и скорее всего будет не совпадать с версией библиотеки используемой для линковки с DLL.

Если в каком-либо из переписанных из EXE в DLL методов применяется один из этих операторов, например «new» для выделения памяти, а для её освобождения будет соответственно использоваться оператор «delete», но уже в недрах EXE модуля, то при работе программы может возникнуть исключение. Поэтому, если в переписанном методе в DLL необходимо использовать отдельный оператор «new» или «delete», предпочтительнее вызывать его по соответствующему адресу из EXE модуля, определив его предварительно в дизассемблере.

Пример такого поведения содержится в проекте DLL в методе Adder::Add() для создания массива символов.

Ведение логов.

Иногда может оказаться полезным регистрация каких либо событий, происходящих в сторонней программе.

Для реализации такого примера в конструкторе метода Patch тестовой DLL создаётся и открывается на запись файл SimpleCalc.log, а затем в этот файл записываются производимые пользователем операции.

Заключение.

Описанный здесь способ неоднократно помог мне в самых различных ситуациях. С его помощью были исправлены баги и недочёты в десятках программных продуктах для моей частной и профессиональной деятельности и я по сей день прибегаю к его использованию. Он одинаково хорошо применим для работы с 32-х и 64-х разрядными приложениями и апробирован на различных версиях ОС Windows, так что его смело можно рекомендовать к практическому применению при минимальных трудозатратах на внедрение.

Спасибо за проявленный интерес!

Приложение.

Архивный файл с проектом программы SimpleCalc и тестовой DLL для VS.

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


  1. couragic
    01.07.2022 14:52
    +1

    Спасибо. Добавил в закладки.


  1. kmeaw
    01.07.2022 15:06
    +1

    Часто возникает соблазн внедрить свой код в чужой файл без какой бы то нибыло его модификации.

    Самый простой способ: создать процесс с чужим кодом с помощью CreateProcess()с dwCreationFlags=CREATE_SUSPENDED, а затем выделить новую страницу памяти (VirtualAllocEx с hProcess и flAllocationType=MEM_COMMIT|MEM_RESERVE), потом WriteProcessMemory запишет в неё имя нашей DLL. И останется только запустить ещё один поток (CreateRemoteThread), который вызовет LoadLibrary. Сложности будут, если чужой код собран под 32-битную архитектуру, а внедряющая программа - под 64 бита, но это тоже решаемо. И в конце останется только разбудить чужой процесс (ResumeThread).


    1. beketata Автор
      01.07.2022 15:53
      +1

      Самый простой способ: создать процесс с чужим кодом с помощью CreateProcess()

      Вопрос был не столько в том, как можно не модифицировать EXE файл, но и как ничего не изменять в процедуре запуска этого приложения, которое может стартовать, скажем, через ярлык на рабочем столе, или как-то ещё - изнутри, например, другой программы.
      Комильфо - это было бы поместить дополнительную DLL в каталог с EXE файлом и ничего больше не трогать.
      Описываемый Вами способ конечно, простой, только чтобы создать этот процесс понадобится дополнительный EXE файл, который будет запускать из себя отлаживаемый. А здесь могут возникнуть свои грабли, например с передаваемыми в программу параметрами, или названием этого дополнительного файла. Так что - "хрен редьки не слаще". Хотя, конечно же, можно и так.


  1. spag002
    04.07.2022 10:03
    +1

    Прочитал по диагонали - не пойму, кто ЦА этой статьи?

    Для новичков слишком сложно, для опытных вообще не интересно, а интересующимся на васм.ру еще 15 лет назад все разжевали. Новичкам подошел бы OllyDbg, а тут и IDA, и импорт, и написание dll, и классическая ошибка всех плохих учебников: "а теперь напишем этот набор символов - вуаля, работает!"

    0xFF, 0x25, 0x00, 0x00, 0x00, 0x00

    Но иногда встречаются EXE файлы, у которых в заголовке в параметре IMAGE_NT_HEADER.OptionalHeader.DllCharacteristics установлен флаг «DLL Can Move» и в этом случае ОС вправе загрузить его по любому другому адресу.

    В 99% случаев dll и будет загружена по произвольному адресу, а не иногда. Именно за этим и придуманы релоки.

    Короче, какая-то сборная солянка, вдобавок "несъедобная".

    P.S. И да, комментатор выше прав - для таких задач больше подойдет лоадер. А для "комильфо" рекомендую гуглить: перенаправление dll.