Как сохранить адрес исходной функции и вызвать ее позже?

Я пытаюсь вызвать исходную функцию в хуке API, который предотвращает инъекции dll с помощью LdrLoadDll(), но каждый раз, когда я пытаюсь загрузить DLL, отличную от фильтрованной, приложение вылетает и не может вызвать исходную функцию. Кажется, я делаю что-то не так, когда сохраняю «original_function» перед хуком.

Я тестирую Windows 7 x64, вводя 32-битную dll в 32-битное приложение (код ниже).

Как это исправить?

program Project1;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Windows,
  SysUtils;

type
  NTSTATUS = Cardinal;
  PUNICODE_STRING = ^UNICODE_STRING;

  UNICODE_STRING = packed record
    Length: Word;
    MaximumLength: Word;
    Buffer: PWideChar;
  end;

const
  STATUS_ACCESS_DENIED = NTSTATUS($C0000022);

var
  Old_LdrLoadDll: function(szcwPath: PWideChar; dwFlags: DWORD;
    pUniModuleName: PUNICODE_STRING; pResultInstance: PPointer)
    : NTSTATUS; stdcall;

function LdrLoadDll(szcwPath: PWideChar; dwFlags: DWORD;
  pUniModuleName: PUNICODE_STRING; pResultInstance: PPointer)
  : NTSTATUS; stdcall;
begin
  Result := Old_LdrLoadDll(szcwPath, dwFlags, pUniModuleName, pResultInstance);
end;

procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
  OldProtect: DWORD;
begin
  if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
  begin
    Move(NewCode, Address^, Size);
    FlushInstructionCache(GetCurrentProcess, Address, Size);
    VirtualProtect(Address, Size, OldProtect, @OldProtect);
  end;
end;

type
  PInstruction = ^TInstruction;

  TInstruction = packed record
    Opcode: Byte;
    Offset: Integer;
  end;

procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
  NewCode: TInstruction;
begin
  NewCode.Opcode := $E9;
  NewCode.Offset := NativeInt(NewAddress) - NativeInt(OldAddress) -
    SizeOf(NewCode);
  PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;

function NewLdrLoadDll(szcwPath: PWideChar; dwFlags: DWORD;
  pUniModuleName: PUNICODE_STRING; pResultInstance: PPointer)
  : NTSTATUS; stdcall;
begin
  if (pos('111', pUniModuleName.Buffer) > 0) or
    (pos('222', pUniModuleName.Buffer) > 0) then

    Result := STATUS_ACCESS_DENIED
  else
    Result := LdrLoadDll(szcwPath, dwFlags, pUniModuleName, pResultInstance);
end;

begin
  @Old_LdrLoadDll := GetProcAddress(GetModuleHandle('ntdll.dll'), 'LdrLoadDll');
  try
    RedirectProcedure(GetProcAddress(GetModuleHandle('ntdll.dll'),
      'LdrLoadDll'), @NewLdrLoadDll);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;

end.

person Community    schedule 24.01.2019    source источник
comment
Вы подключаете его, размещая переход в начале исходной процедуры. Это также означает, что когда вы вызываете исходную процедуру, вы просто снова столкнетесь с этим переходом, снова перейдете к своей новой процедуре и, по сути, в какой-то момент получите переполнение стека из-за слишком глубокой рекурсии.   -  person GolezTrol    schedule 24.01.2019
comment
Я тоже делал что-то подобное (но для функций Delphi, а не библиотечных, хотя разницы может и не быть). Мой код в этом форуме post с небольшим пояснением на голландском языке (правда, комментарии в коде на английском). В любом случае, это довольно просто. Один модуль содержит общий код для подключения и отключения функции, а другой модуль содержит некоторые примеры использования.   -  person GolezTrol    schedule 24.01.2019
comment
В примере даже показано, как восстановить оригинал и вызвать его из кода ловушки, поэтому я думаю, что это именно то, что вам нужно. Другое заметное отличие состоит в том, что я использовал CALL ($E8), а не JMP. Сейчас у меня нет больше времени, но дайте мне знать, помогло ли это, и я могу позже расширить эти комментарии до полного ответа со встроенным кодом.   -  person GolezTrol    schedule 24.01.2019
comment
@GolezTrol, я видел ваш код, кажется, вы удалили хук для вызова исходной функции. Как вызвать исходную функцию, не удаляя хук (аналогично тому, как я пытался сохранить его в переменной)?   -  person    schedule 24.01.2019
comment
Проблема в том, что вы фактически уничтожаете код исходной функции, помещая туда свой прыжок. Фактически вы перезаписываете первую часть этой функции. Итак, вы сохранили указатель на позицию исходной функции, но это не дает исходного кода. Я почти уверен, что вам нужно удалить хук (то есть восстановить исходный код), чтобы иметь возможность правильно вызывать исходную функцию. Но данный класс делает это тривиальным. Это может быть проблемой, если вам нужно сделать это из нескольких потоков одновременно...   -  person GolezTrol    schedule 24.01.2019
comment
@Davison: я уверен, что можно написать код, который анализирует заменяемый код, а затем поместить его куда-нибудь вместе с переходом к после вашего JMP, но поскольку у вас его нет, вы должен заменить исходный код, а затем перейти к исходному адресу (снова), чтобы все заработало. Скажем, исходный код находится по адресу N. Ваш переход использует, скажем, 5, 5 байт. Вы не можете просто перейти к N+5, потому что код изначально по адресу N, т.е. PUSH BP и т. д. тогда не выполняются, и N+5 может находиться в середине инструкции. Как правило, вы должны заменить старый код.   -  person Rudy Velthuis    schedule 24.01.2019
comment
FWIW, для Delphi есть несколько очень хороших обходных библиотек. Найдите их и используйте один из них. Они делают правильные вещи и помогают с более сложными проблемами.   -  person Rudy Velthuis    schedule 24.01.2019
comment
@RudyVelthuis Нравится MahdiSafsafi/DDetours? Это кажется немного более полным, чем моя версия. По крайней мере, это примерно в 100 раз больше кода. :)   -  person GolezTrol    schedule 24.01.2019
comment
@GolezTrol, вы могли бы дать ответ, включающий все основные части ваших предыдущих комментариев и код примера (возможное решение), основанный на том, что было опубликовано выше (по вопросу), пожалуйста?   -  person    schedule 24.01.2019
comment
@GolezTrol: действительно. Мне он никогда не был нужен, но слышал (ну... читал) о нем только хорошее.   -  person Rudy Velthuis    schedule 24.01.2019


Ответы (2)


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

Microsoft Detours теперь бесплатна и имеет открытый исходный код. А поскольку он собирается в библиотеки DLL, вы можете использовать его с Delphi.

Вам понадобится компилятор C++, чтобы собрать его из исходного кода, но опять же Visual Studio Community бесплатен.

person Alex Guteniev    schedule 24.01.2019
comment
FWIW, сообщество C++Builder тоже бесплатно. Но проект MS, вероятно, лучше скомпилировать компилятором MS. - person Rudy Velthuis; 24.01.2019
comment
Я не люблю Microsoft Detours. @GolezTrol прав: "That seems to be quite a bit more comprehensive than my version. At least it's about 100 times as much code. :)" - person ; 25.01.2019
comment
Вот почему я предлагаю это. Поскольку он всеобъемлющий, он все делает правильно. Перед перезаписью целевого кода t копирует старый код в новое место. И это убирает старую функцию, чтобы скопировать всю инструкцию и не сломать инструкцию в середине. Он также сдается, если ему не удается сделать такую ​​​​копию. - person Alex Guteniev; 25.01.2019
comment
По сути, вы не можете надежно решить такую ​​задачу с объемом кода, предоставленным OP. Решения без дизассемблера будут работать до тех пор, пока не появится следующая реализация целевой функции. - person Alex Guteniev; 25.01.2019
comment
Мое замечание касалось не версии Microsoft, а версии Delphi, на которую я ссылался. Но на самом деле я намеревался сделать в 100 раз больше замечаний, чтобы подчеркнуть, что он учитывает все виды ситуаций, что в целом сделало бы его лучшим/более безопасным решением, чем мое собственное. Хотя мое решение без проблем использовалось в течение многих лет в производственном (однопоточном) приложении для перехвата какой-либо функции VCL, иногда достаточно этой пары строк. - person GolezTrol; 25.01.2019
comment
Функция VCL в порядке. Но, если вы перехватываете системные функции, такие как LdrLoadDll, будьте готовы, что она вызывается не только вами, но и системой, и сторонним программным обеспечением, внедренным в ваш процесс (например, антивредоносным ПО). А однопоточных сейчас в винде нет. ОС и стороннее программное обеспечение будут создавать потоки в вашем приложении без вашего согласия. Поэтому упрощенный подход, не использующий табличный дизассемблер (например, обходные пути), приведет к сбоям. - person Alex Guteniev; 26.01.2019

Вот один рабочий код, сделанный после того, как вы следовали предложениям @GolezTrol:

program Project1;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Windows,
  SysUtils;

type
  NTSTATUS = Cardinal;
  PUNICODE_STRING = ^UNICODE_STRING;

  UNICODE_STRING = packed record
    Length: Word;
    MaximumLength: Word;
    Buffer: PWideChar;
  end;

const
  STATUS_ACCESS_DENIED = NTSTATUS($C0000022);

var
  Old_LdrLoadDll: function(szcwPath: PWideChar; dwFlags: DWORD;
    pUniModuleName: PUNICODE_STRING; pResultInstance: PPointer)
    : NTSTATUS; stdcall;

function LdrLoadDll(szcwPath: PWideChar; dwFlags: DWORD;
  pUniModuleName: PUNICODE_STRING; pResultInstance: PPointer)
  : NTSTATUS; stdcall;
begin
  Result := Old_LdrLoadDll(szcwPath, dwFlags, pUniModuleName, pResultInstance);
end;

type
  PInstruction = ^TInstruction;

  TInstruction = packed record
    Opcode: Byte;
    Offset: Integer;
  end;

//======= Structure to store original function ======== 

type
  TSaveOriginal = packed record
    Addr: Pointer;
    Bytes: array [0 .. SizeOf(TInstruction)] of Byte;
  end;

  PSaveOriginal = ^TSaveOriginal;

var
  SaveOriginal: TSaveOriginal;

//====================================================

procedure PatchCode(Address: Pointer; const NewCode; Size: Integer;
  SaveOriginal: PSaveOriginal);
var
  OldProtect: DWORD;
begin
  if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
  begin
    //======== Saving original function =========
    if Assigned(SaveOriginal) then
    begin
      SaveOriginal^.Addr := Address;
      Move(Address^, SaveOriginal^.Bytes, Size);
    end;
    //===========================================
    Move(NewCode, Address^, Size);
    FlushInstructionCache(GetCurrentProcess, Address, Size);
    VirtualProtect(Address, Size, OldProtect, @OldProtect);
  end;
end;

procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
  NewCode: TInstruction;
begin
  NewCode.Opcode := $E9;
  NewCode.Offset := NativeInt(NewAddress) - NativeInt(OldAddress) -
    SizeOf(NewCode);
  PatchCode(OldAddress, NewCode, SizeOf(NewCode), @SaveOriginal);
end;

procedure UndoRedirectProcedure(const SaveOriginal: TSaveOriginal);
var
  OldProtect: Cardinal;
begin
  if not VirtualProtect(SaveOriginal.Addr, SizeOf(TInstruction),
    PAGE_EXECUTE_READWRITE, OldProtect) then
    RaiseLastOSError;
  Move(SaveOriginal.Bytes, SaveOriginal.Addr^, SizeOf(TInstruction));
  if not VirtualProtect(SaveOriginal.Addr, SizeOf(TInstruction), OldProtect,
    OldProtect) then
    RaiseLastOSError;
end;

function NewLdrLoadDll(szcwPath: PWideChar; dwFlags: DWORD;
  pUniModuleName: PUNICODE_STRING; pResultInstance: PPointer)
  : NTSTATUS; stdcall;
begin
  if (pos('111', pUniModuleName.Buffer) > 0) or
     (pos('222', pUniModuleName.Buffer) > 0) then

    Result := STATUS_ACCESS_DENIED
  else
  begin

    UndoRedirectProcedure(SaveOriginal); // Restore original function

    @Old_LdrLoadDll := SaveOriginal.Addr;

    Result := LdrLoadDll(szcwPath, dwFlags, pUniModuleName, pResultInstance); // Call original function

    RedirectProcedure(GetProcAddress(GetModuleHandle('ntdll.dll'),
      'LdrLoadDll'), @NewLdrLoadDll); // Hook again
  end;
end;

begin
  try
    RedirectProcedure(GetProcAddress(GetModuleHandle('ntdll.dll'),
      'LdrLoadDll'), @NewLdrLoadDll); 
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;

end.
person Community    schedule 25.01.2019
comment
Это ненадежно в случае многопоточных вызовов целевой функции. - person Alex Guteniev; 25.01.2019
comment
В конкретном случае LdrLoadDll вы можете получить блокировку загрузчика перед записью и вызовом исходной функции. Но в общем случае решение не очень хорошее, так как обычно функции должны вызываться одновременно без дополнительных блокировок. - person Alex Guteniev; 25.01.2019