如何保存原始函数的地址并在以后调用它?

Posted

技术标签:

【中文标题】如何保存原始函数的地址并在以后调用它?【英文标题】:How save the address of original function and call this later? 【发布时间】:2019-06-18 08:16:17 【问题描述】:

我正在尝试在 api 挂钩中调用原始函数,以防止 LdrLoadDll() 函数注入 dll,但每次我尝试加载过滤后的不同 dll 时,应用程序崩溃并且不可能调用原始函数。似乎我什么时候会在钩子之前保存“original_function”时做错了。

我在 Windows 7 x64 上进行测试,在 32 位应用程序中注入 32 位 dll(代码如下)。

如何解决这个问题?

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.

【问题讨论】:

您通过在原始过程的开头放置一个跳转来挂钩它。这也意味着,当您调用该原始过程时,您将再次遇到该跳转,再次跳转到您的新过程,并且基本上在某个点由于递归太深而导致堆栈溢出。 我也做过类似的事情(但对于 Delphi 函数,而不是库函数,尽管可能没有区别)。我的代码是in this forum post,用荷兰语做了一些解释(不过,代码中的 cmets 是英文的)。无论如何,这很简单。一个单元包含用于挂钩和取消挂钩函数的通用代码,另一个单元包含一些示例用法。 这个例子甚至展示了如何恢复原始文件并从你的钩子代码中调用它,所以我认为这正是你想要的。另一个显着的区别是我使用了 CALL ($E8) 而不是 JMP。我现在没有更多时间,但如果它成功了,请告诉我,我可能会在稍后使用内联代码将这些 cmets 扩展为完整答案。 @GolezTrol,我看到了你的代码,似乎你删除了调用原始函数的钩子。如何在不移除钩子的情况下调用原始函数(类似于我尝试将其保存到变量)? 问题是,你基本上通过把你的跳转放在那里破坏了原始函数的代码。您实际上覆盖了该函数的第一部分。因此,您已经存储了指向原始函数位置的指针,但这并没有给您原始代码。我很确定您必须删除钩子(即恢复原始代码)才能正确调用原始函数。但是给定的课程让这件事变得微不足道。如果您需要同时从多个线程执行此操作,这可能是一个问题...... 【参考方案1】:

我建议你不要使用自制的钩子,并为此获取现有的库。

Microsoft Detours 现在是免费和开源的。当它构建为 DLL 时,您可以将它与 Delphi 一起使用。

您将需要 C++ 编译器从源代码构建它,但 Visual Studio 社区又是免费的。

【讨论】:

FWIW,C++Builder 社区也是免费的。但是使用 MS 编译器编译 MS 项目可能会更好。 我不喜欢 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. :)" 这就是我建议它的原因。因为它是全面的,所以它正确。在覆盖目标代码之前,t 将旧代码复制到新位置。并且它拆解了旧功能,复制全部指令并避免在中间破坏指令。如果无法进行此类复制,它也会放弃 基本上,您无法使用 OP 提供的大量代码可靠地解决此类任务。没有反汇编程序的解决方案将一直有效,直到目标函数的下一个实现到达 我的评论不是关于Microsoft版本,而是关于我链接到的Delphi版本。但实际上,我打算使用“100 倍”的评论来强调它考虑了各种情况,这通常会使其成为比我自己的更好/更安全的解决方案。虽然我的解决方案在(单线程)生产应用程序中使用了多年,但没有出现任何问题,可以挂钩一些 VCL 功能,所以有时这几行就足够了。【参考方案2】:

这是遵循@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.

【讨论】:

多线程调用目标函数时不可靠。 对于特定的 LdrLoadDll 情况,您可以在编写和调用原始函数之前获取加载程序锁。但是对于一般情况,解决方案并不好,因为通常函数应该可以同时调用而无需额外的锁。

以上是关于如何保存原始函数的地址并在以后调用它?的主要内容,如果未能解决你的问题,请参考以下文章

Stripe.js API:有没有办法让用户保存卡信息并在以后使用它进行传输?

在函数调用之前保存 XMM 寄存器

如何从 jited .Net 函数的原始地址获取源代码信息?

用户如何保存表单并在以后访问它?

如何将函数作为道具传递给子组件并在Vue中从那里调用它?

再说C模块的编写