📜 ⬆️ ⬇️

Implementing API Call Interception

Monitor. Excellent word, however, combines a whole bunch of concepts. Well, for example, for the first time this word was applied in 1861 to the battleship USS Monitor. A little later, this word began to be called displays. After some more, things that were more familiar to us, like performance counters, entered the glorious cohort of monitors, and with them a whole bunch of various software whose main task is monitoring — that is, monitoring.

The task of the monitor is simple - in fact it is an observer, although it can also act as a manager, by the way one of the translation options for “Monitor” is a mentor. Its objectives are also obvious, to give a set of data on the basis of which it is possible to analyze the situation and draw the appropriate conclusions.

In modern software, monitors are found almost everywhere, well, for example, the same Punto Switcher is a classic example of a legal monitor. Almost all antiviruses are monitors, profilers, well, I'm not talking about our main toolkit - the debugger, which is also a monitor.
')
On the reverse side of the barricades, whole heaps of malicious software appear, some of which also prefer to use monitoring to achieve their main goals. But not about them now ...

On the Internet at the moment there is simply an incredible number of examples of software function monitors, although in most cases it is considered interception by editing the import table, or installing an interceptor at the beginning of the intercepted function (so-called splicing). But despite the availability of these materials, sometimes they do not help developers, because they are written too (let's say) abstruse language, or even represent a piece of code torn out of context, incomprehensible to a non-savvy developer.

So it happened with me, over the last month several people addressed me with the question of how to implement the interceptor correctly and the references to the examples practically did not help them, I had to chew everything from scratch. But now I am aware of the basic mistakes encountered by people just starting to deal with interception techniques.

As a result, in order not to explain everything next time again, I decided to make an overview article where I would try to tell everything in as simple a language as possible, “how it works.”


1. The essence of the monitor


The main task of the monitor in any way available to him to learn about the transfer of control to the function he controls.

For this, various variants of intercepting calls to the original function are used, with the help of which control is transferred to the “interception handler” (or “interceptor”, to whom it is more convenient).

What to do with the intercepted function further depends on the implementation of the interceptor. You can display in the log the parameters of its call, well, if desired, other parameters, such as its results, the state of the call stack, etc. The main thing is to take control over yourself.

I can say that you almost guaranteed to have previously written function interceptors yourself. The concept of OOP greatly contributes to this. Any class in which you overlap virtual or dynamic methods can already be called monitored by your monitor. After all, in reality, override methods that are overlapped are directly final handlers of intercepted functions, and we don’t even have to think about how it actually works inside there and the challenge of the original method will not even cause difficulties for us, there is a regular inherited call for this. The compiler has already done everything for us.

But we have to dig a little deeper. For example, with static methods such a focus will not pass and if there is a need for such an overlap, you will have to do everything yourself. However, before proceeding directly to monitoring, it is necessary to deal with the implementation of the interception handler.

2. Correct interception handler declaration


Before proceeding to the interception of any function, it is necessary to know the parameters of its call and the calling convention. That is, if we want to intercept for example MessageBoxA, then the interceptor, which will work instead of the original function, should have the following form:

function InterceptedMessageBoxA(hWnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; begin // TODO... end; 

That is, the parameters of the interception handler and the calling convention (stdcall / cdecl, etc.) must exactly match the original function. If this is not done, there will almost certainly be an error after calling the handler.

The correct handler declaration is needed primarily for ease of operation. You can, of course, write such a handler:

 procedure InterceptedFunc; begin // TODO... end; 

But in this case, it will be necessary to get the call parameters and make the correct call finalization taking into account agreements and the number of parameters using assembler inserts.

A small nuance will be with the implementation of the function / procedure interceptor implemented in the form of class methods.

Suppose we intercept a TApplication.MessageBox.
If the interception handler is implemented as a class method, then its declaration will look like the original function:

 function TTestClas.InterceptedApplicationMessageBox( const Text, Caption: PChar; Flags: Longint): Integer; begin // TODO... end; 

If the interceptor is an independent function, then its implementation will look a little different:

 function InterceptedApplicationMessageBox( Self: TObject; const Text, Caption: PChar; Flags: Longint): Integer; begin // TODO... end; 

Such differences in the handler's declaration are due to the fact that the class parameter of the class methods is not the explicitly declared Self variable.

By the way, if you try in the first interceptor to get the class name of this variable, for example like this:

 function TTestClas.InterceptedApplicationMessageBox( const Text, Caption: PChar; Flags: Longint): Integer; begin ShowMessage(Self.ClassName); end; 

... then the text TApplication will be displayed, not TTestClass, since Self parameter will contain data about the original class, and not about the class in which the interceptor is implemented.

In principle, this is all you need to know about the interception handler declaration, you can now start to consider various ways to intercept functions.

3. Subclassing window procedure


For a long time I chose where to start the technical part and eventually decided to dwell on documented methods. Indeed, why reinvent the next bike - it's easier to bend the one that is.

I do not think that it will be a secret to you that the VCL is essentially just a wrapper over the API. Most of the visual elements on the form, and the form itself is a window, which, among other parameters, has a window procedure. If a programmer has a task that requires changing the standard behavior of the window, he applies subclassing, replacing the window procedure handler with his own, where he implements the functionality he needs.

The technique is quite common, and when implementing your own controls in Delphi, you almost always encounter the result of overlapping the window procedure of all your windows with the global handler TWinControl.MainWndProc. This is really quite a convenient solution that allows us to override certain messages in the code by specifying message + message constant, working with the virtual WndProc and DefaultHandler, and so on ...

A general description of this method can be found at this link: Subclassing a Window

The implementation algorithm can be presented in the form of five points:
  1. getting the address of the original window procedure
  2. storing it in any place accessible to the processor
  3. assigning a new window procedure handler
  4. processing the interceptor call (logging / changing call parameters, etc.)
  5. if necessary, obtain the address of the old handler and call it.

In the form of code, everything looks quite simple.

 unit uSubClass; interface uses Windows, Messages, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} function MainFormSubclassProc(hwnd: THandle; uMsg: UINT; wParam: WPARAM; lParam: WPARAM): LRESULT; stdcall; var OldWndProc: Pointer; begin if uMsg = WM_WINDOWPOSCHANGING then PWindowPos(lParam)^.flags := PWindowPos(lParam)^.flags or SWP_NOSIZE or SWP_NOMOVE; OldWndProc := Pointer(GetWindowLong(hwnd, GWL_USERDATA)); Result := CallWindowProc(OldWndProc, hwnd, uMsg, wParam, lParam); end; procedure TForm1.Button1Click(Sender: TObject); var OldWndProc: THandle; begin OldWndProc := GetWindowLong(Handle, GWL_WNDPROC); SetWindowLong(Handle, GWL_USERDATA, OldWndProc); SetWindowLong(Handle, GWL_WNDPROC, Integer(@MainFormSubclassProc)); MessageBox(Handle, '      ', PChar(Application.Title), MB_ICONINFORMATION); end; end. 

In this example, the window procedure of the main form of the application is replaced.
The address of the old procedure is stored in the user buffer of the window, which is accessed through the constant GWL_USERDATA.
When you call the new MainFormSubclassProc handler, the message code is checked. If this message is about resizing or coordinates of a window, then this feature is blocked by setting the SWP_NOSIZE and SWP_NOMOVE flags.
Try to run this application and click on the button and then try to resize the main form. You will not succeed.

Small nuance : in this code there is no double overlap check. If you hit the button a second time, you will get an error about stack overflow. This is caused by the fact that the second overlapping of the window procedure, when you receive the address of the old one, returns the same address of the MainFormSubclassProc handler and, accordingly, when you first call CallWindowProc, you will enter an infinite loop, since we will call ourselves, the exit from which will occur overflow.

Application : This interception option can only be used for windows. For conversations, you must use the constant DWL_DLGPROC.

As a result : within the framework of the monitor concept, the old window procedure acts as a monitored function here, the new MainFormSubclassProc handler is used as a monitor in which we monitor all data coming to the monitored function. In it, we can log all parameters, as well as control the behavior of the old handler by replacing the data before calling it, or not calling it at all, and implement our own behavior.

Unfortunately, this code will only work for windows of our application. Well, more precisely, we can of course get the window handle from someone else's application and assign it a new interceptor in the same way as shown above, but this will not lead to anything good, because in this case, we will specify the address of the new handler, which is located in our address space, and in another address there will be a completely different code (well, or in general this section of memory may not be allocated), which may lead to the sudden death of someone else's process.

To prevent this from happening, you need to somehow place the interceptor code in another process and only after that do the substitution. This is done in several ways, but we will dwell on them a little later ...

4. Interception by editing VMT tables.


In principle, this is a very rare variant of interception, but since I decided to tell about all possible methods, I will also have to consider it.

Probably, you have encountered situations when the behavior of a particular control completely suits you, but a little something is missing. To solve a problem, you usually have to write an inheritor of the problem class, in which you override the corresponding virtual methods, where you write the desired behavior of the control. But if it’s too lazy, this can be achieved without implementing an heir, simply by overriding the required method from the outside.

In the following example, I will show how to override the TForm.CanResize method that is called when resizing a form. The task of the code is to prevent resizing the form to a width greater than 500 pixels.

Of course, in this example, a whole bunch of minuses, firstly, to achieve the goal, it was possible to block the OnResize handler, and secondly, since TForm's CanResize overlaps, this modification will affect all forms in the project, but he and an example - his task is simply show this opportunity.

Application : This interception option can be used only for virtual methods of classes, and only within its own PE file. If you put this code into the library and try to intercept the method from the main application in this way, it will not work, since the TForm applications and the TForm libraries are different classes.

The code looks like this:

 unit uVMT; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} function NewCanResizeHandler(Self: TObject; var NewWidth, NewHeight: Integer): Boolean; begin Result := True; if NewWidth > 500 then NewWidth := 500; end; procedure TForm1.Button1Click(Sender: TObject); var VMTAddr: PPointer; OldProtect: Cardinal; begin asm mov eax, Self mov eax, [eax] //    VMT  add eax, VMTOFFSET TForm.CanResize //     TForm.CanResize mov VMTAddr, eax end; //     VirtualProtect(VMTAddr, 4, PAGE_EXECUTE_READWRITE, OldProtect); try //    VMTAddr^ := @NewCanResizeHandler; finally //    VirtualProtect(Pointer(VMTAddr), 4, OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, VMTAddr, 4); end; if Width > 500 then Width := 500; MessageBox(Handle, '     500 ', PChar(Application.Title), MB_ICONINFORMATION); end; end. 

Now in more detail:

1. The declaration of the interception handler is made taking into account the nuances described in the second section of this article (the Self parameter has appeared).

2. To intercept, the documented VMTOFFSET directive is used, described in the Delphi help.

Here is a piece with its description:
Two additional directives and VMTFFSET and DMTINDEX.
It’s a way to get a virtual guideline. For example, TExample.VirtualMethod.

As is clear from the description, its task is to return the offset to the address of the virtual method relative to the beginning of the virtual method table (VMT). At the beginning of the VMT table directly indicates the parameter Self. That is, if you portray it as a code, you get the following:

 VMT := Pointer(Self)^; 

3. After using the assembler insert to get the pointer to the address of the method, it is replaced by the address of the new virtual method handler.

4. Since the substitution is made in the application code, which is usually located in the memory page, which does not have write permissions (usually these are read and execute rights), before assigning a new handler, write permissions are set.

Now, run the example and test it.

As you can see, in fact, we have performed almost all the same steps as in the first example, i.e. received the address of the monitored function, and replaced it with the address of the new handler, in which the parameters of the original method are managed. I will not give the code to call the original handler (analogue of inherited ), since Yet this method of interception is shown only for broadening horizons and is highly undesirable for implementation in a combat application.

If you have trouble understanding the VMT editing principle, I can recommend this article to be read, by Hallvard Vassbotn : Method calls compiler implementation.

Or its translation, kindly provided by Alexander Alekseev : Implementation of method calls by the compiler .

I will not consider the variant with the interception of dynamic class methods - but the principle is approximately the same.

5. Interception by editing the import table


What is an import table. When you write an application and call API functions, the application must somehow calculate the address of this function in order to transfer control to it. Most of the functions are declared statically, well, for example:

 {$EXTERNALSYM MessageBox} function MessageBox(hWnd: HWND; lpText, lpCaption: PChar; uType: UINT): Integer; stdcall; ... function MessageBox; external user32 name 'MessageBoxA'; 

This clearly indicates the complete declaration of calling the MessageBox function and the calling convention. This information is required by the compiler to properly align the stack when the function is called. It also indicates the name of the library in which the function is implemented and the name under which it is exported.

As you can see, the function addresses are not here, and it cannot be, because the library, the export function can be loaded at any address and in most cases the address of the same function will be different for each process. The truth is there is a small nuance, the user32.dll, kernel32.dll and ntdll.dll libraries are loaded for all processes at a fixed address (at least in 32-bit systems), but you still should not rely on a certain static address.

When compiling an application, information about statically declared functions is placed in the body of the import table. At the moment of application launch, the loader analyzes this table, loads the libraries specified in it, on the basis of the library's export table, finds out the real address of the function, which it places in the corresponding field of the application's import table.

A general description of the principle of the import table can be found in Matt Pitrek's article : A Tour of the Win32 Portable Executable File Format .
Its translation is available on the RSDN: PE and COFF formats of object files .
If you need more detailed information, you can study the following article: PE. Lesson 6. Import table for Iczelion authorship.

Now what can be done with this information? Everything is simple, to assign an interception handler, you just need to change the address calculated by the loader to the address of the handler, after which the interception can be considered valid.

Application : This interception option applies only to API functions with static linking.

To search for the address of the original function and replace it with the address of the interceptor, we write the following function:

 function ReplaceIATEntry(const OldProc, NewProc: FARPROC): Boolean; var ImportEntry: PImageImportDescriptor; Thunk: PImageThunkData; Protect: DWORD; ImageBase: Cardinal; DOSHeader: PImageDosHeader; NTHeader: PImageNtHeaders; begin Result := False; if OldProc = nil then Exit; if NewProc = nil then Exit; ImageBase := GetModuleHandle(nil); //     DOSHeader := PImageDosHeader(ImageBase); NTHeader := PImageNtHeaders(DWORD(DOSHeader) + DWORD(DOSHeader^._lfanew)); ImportEntry := PImageImportDescriptor(DWORD(ImageBase) + DWORD(NTHeader^.OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)); //     ... while ImportEntry^.Name <> 0 do begin Thunk := PImageThunkData(DWORD(ImageBase) + DWORD(ImportEntry^.FirstThunk)); // ...     ... while Pointer(Thunk^._function) <> nil do begin // ...      . if Pointer(Thunk^._function) = OldProc then begin //   if VirtualProtect(@Thunk^._function, SizeOf(DWORD), PAGE_EXECUTE_READWRITE, Protect) then try //   ... //Thunk^._function := DWORD(NewProc); // ...   . InterlockedExchange(Integer(Thunk^._function), Integer(NewProc)); Result := True; finally VirtualProtect(@Thunk^._function, SizeOf(DWORD), Protect, Protect); FlushInstructionCache(GetCurrentProcess, @Thunk^._function, SizeOf(DWORD)); end; end else Inc(PAnsiChar(Thunk), SizeOf(TImageThunkData32)); end; ImportEntry := Pointer(Integer(ImportEntry) + SizeOf(TImageImportDescriptor)); end; end; 

It edits the import table in the body of the current executable file, as indicated by the code for obtaining the image base GetModuleHandle (nil). Directly setting the address of the interceptor is done atomically, by calling InterlockedExchange. This is a rather subtle point, however, I’m going to dwell on the reasons for just this way of changing the address of the interceptor.

Now it remains to write an example of calling this function.

Create a new project, place a button on the main form, record the implementation of the ReplaceIATEntry function, and then add the following code:

 var OrigAddr: Pointer = nil; function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin S := AnsiString('Function interepted. Original message: ' + lpText); Result := TOrigMessageBoxA(OrigAddr)(Wnd, PAnsiChar(S), lpCaption, uType); end; procedure TForm1.FormCreate(Sender: TObject); begin OrigAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); ReplaceIATEntry(OrigAddr, @InterceptedMessageBoxA); end; procedure TForm1.Button1Click(Sender: TObject); begin MessageBoxA(0, 'Test Message', nil, 0); end; 

Here, the form designer sets the interception of the MessageBoxA function, and in the button handler, MessageBoxA is called to demonstrate how the interception works.

The interception handler itself is very simple. Since only the entry in the import table was changed and the body of the original function did not correct, to transfer control to the original function, it is enough to make a call to the previously stored address of the function, which is stored in the variable OrigAddr.

There is a small nuance :
The static function can be located in the deferred import section. Such a declaration is made quite simply, let's change the sample code a bit like this:

  function DelayedMessageBoxA(hWnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; external user32 name 'MessageBoxA' delayed; procedure TForm1.Button1Click(Sender: TObject); begin DelayedMessageBoxA(0, 'Test Message', nil, 0); end; 

The DelayedMessageBoxA function is actually the same MessageBoxA.
But the " delayed " parameter registers the call to this function in the deferred import section.

Nuance : the " delayed " parameter is not available in earlier versions of Delphi, so you will not find this piece of code in the example examples for the article.

Since the interceptor implemented above makes changes only in the import table, the function call declared in this way will not be controlled by it. To do this, you need to make edits in IMAGE DIRECTORY_ENTRY DELAYED IMPORT.

The option of editing the deferred import table is not considered in the article, in principle there is nothing interesting in it, the principle is the same as with regular import, only slightly different structures are used. (If you are interested, we will consider the implementation of this interceptor as your homework :).

6. Interception by editing the export table.


Editing the import and deferred import tables is great for statically declared functions, but will not work against dynamic declaration and function calls.

If in simple terms, try adding another button to the previous example and write the following code in its handler:

 procedure TForm1.Button2Click(Sender: TObject); type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var OrigMessageBoxA: TOrigMessageBoxA; begin @OrigMessageBoxA := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); OrigMessageBoxA(0, 'Test Message', nil, 0); end; 

Click the button and you will see that the interceptor did not work. The fact is that the function is GetProcAddress, which receives the address of the function bypassing the import table, focusing on the library export table.

In order to force the interception handler to respond to the functions called in the above shown way, you need to make changes directly to the export table of the required library.

In brief, the export table contains information about the functions exported by PE file, their name, index (Ordinal) and address of the function. Some functions are exported only by index and do not have a name; in such cases, access to them is made only by means of an index.

.
, :
PE. 7. Iczelion-.

: API .

, . :

 unit uEAT; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); procedure FormCreate(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} uses DeclaredTypes; function ReplaceEATEntry(const DllName: string; OldProc, NewProc: FARPROC): Boolean; var ImageBase: Cardinal; DOSHeader: PImageDosHeader; NTHeader: PImageNtHeaders; ExportDirectory: PImageExportDirectory; pFuntionAddr: PDWORD; OrdinalCursor: PWORD; Ordinal, Protect: DWORD; FuntionAddr: FARPROC; I: Integer; begin Result := False; if OldProc = nil then Exit; if NewProc = nil then Exit; ImageBase := GetModuleHandle(PChar(DllName)); //     DOSHeader := PImageDosHeader(ImageBase); NTHeader := PImageNtHeaders(DWORD(DOSHeader) + DWORD(DOSHeader^._lfanew)); ExportDirectory := PImageExportDirectory(DWORD(ImageBase) + DWORD(NTHeader^.OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)); I := 1; //     OrdinalCursor := Pointer(ImageBase + DWORD(ExportDirectory^.AddressOfNameOrdinals)); while I < Integer(ExportDirectory^.NumberOfNames) do begin //       Ordinal := OrdinalCursor^; //       FuntionAddr := Pointer(ImageBase + DWORD(ExportDirectory^.AddressOfFunctions)); FuntionAddr := Pointer(ImageBase + PDWORD(DWORD(FuntionAddr) + Ordinal * 4)^); // ,    ? if FuntionAddr = OldProc then begin //  ,  ,      pFuntionAddr := PDWORD(ImageBase + DWORD(ExportDirectory^.AddressOfFunctions) + Ordinal * 4); //        hInstance  NewProc := Pointer(DWORD(NewProc) - ImageBase); //    if VirtualProtect(pFuntionAddr, SizeOf(DWORD), PAGE_EXECUTE_READWRITE, Protect) then try //   ... //pFuntionAddr^ := Integer(NewProc); // ...   . InterlockedExchange(Integer(PImageThunkData(pFuntionAddr)^._function), Integer(NewProc)); Result := True; finally VirtualProtect(pFuntionAddr, SizeOf(DWORD), Protect, Protect); FlushInstructionCache(GetCurrentProcess, pFuntionAddr, SizeOf(DWORD)); end; Break; end; Inc(I); Inc(OrdinalCursor); end; end; var OrigAddr: Pointer = nil; function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin S := AnsiString('Function interepted. Original message: ' + lpText); Result := TOrigMessageBoxA(OrigAddr)(Wnd, PAnsiChar(S), lpCaption, uType); end; procedure TForm1.FormCreate(Sender: TObject); begin OrigAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); ReplaceEATEntry(user32, OrigAddr, @InterceptedMessageBoxA); end; procedure TForm1.Button1Click(Sender: TObject); type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var OrigMessageBoxA: TOrigMessageBoxA; begin @OrigMessageBoxA := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); OrigMessageBoxA(0, 'Test Message', nil, 0); end; end. 

, .

7. .


. , - - ( ) . , . , , API , :)

API , — , .

, .

, - .

, , - , .

— , . , . , , , .. - .
, .

- , ?
- «JMP 100» . , . , , .
, , . . , - .

, .

1. JMP NEAR OFFSET
, $E9, JMP NEAR rel32, 4 OFFSET.
OFFSET : OFFSET = DestinationAddr — CurrentAddr —
DestinationAddr —
CurrentAddr — JMP NEAR OFFSET

:

 procedure SliceNearJmp(OldProc, NewProc: FARPROC); var SpliceRec: packed record JmpOpcode: Byte; Offset: DWORD; end; Tmp: DWORD; begin SpliceRec.JmpOpcode := $E9; SpliceRec.Offset := DWORD(NewProc) - DWORD(OldProc) - SizeOf(SpliceRec); VirtualProtect(OldProc, SizeOf(SpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); Move(SpliceRec, OldProc^, SizeOf(SpliceRec)); VirtualProtect(OldProc, SizeOf(SpliceRec), OldProtect, OldProtect); end; 

2. PUSH ADDR + RET
, $68, PUSH imm32, 4 ADDR, , $C3, RET.
. PUSH ADDR RET .

:

 procedure SlicePushRet(OldProc, NewProc: FARPROC); var SpliceRec: packed record PushOpcode: Byte; Offset: FARPROC; RetOpcode: Byte; end; begin SpliceRec.PushOpcode := $68; SpliceRec.Offset := NewProc; SpliceRec.RetOpcode := $C3; VirtualProtect(OldProc, SizeOf(SpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); Move(SpliceRec, OldProc^, SizeOf(SpliceRec)); VirtualProtect(OldProc, SizeOf(SpliceRec), OldProtect, OldProtect); end; 

. .

: MOV EAX, ADDR + JMP EAX. , FASTCALL. , Delphi , EAX, ECX EDX. , EAX . .

. , , HotPatch-.

, , . -, «HotPatch» :)

, MS , API NOP, MOV EDI, EDI ( PUSH xxx)

image

JMP NEAR OFFSET. . , JMP SHORT -7 $EB $F9.

HotPach- :

 procedure SliceHotPath(OldProc, NewProc: FARPROC); var SpliceRec: packed record JmpOpcode: Byte; Offset: DWORD; end; NopAddr: Pointer; OldProtect: DWORD; begin SpliceRec.JmpOpcode := $E9; NopAddr := PAnsiChar(OldProc) - SizeOf(SpliceRec); SpliceRec.Offset := DWORD(NewProc) - DWORD(NopAddr) - SizeOf(SpliceRec); VirtualProtect(NopAddr, 7, PAGE_EXECUTE_READWRITE, OldProtect); Move(SpliceRec, NopAddr^, SizeOf(SpliceRec)); asm mov ax, $F9EB mov ecx, OldProc lock xchg word ptr [ecx], ax end; VirtualProtect(NopAddr, 7, OldProtect, OldProtect); end; 

, .

HotPach- , . - .

, HotPatch- : Create Hotpatchable Image MS VC.

. . , - .

. , :

 unit uNearJmpSplice; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} type //      JMP NEAR OFFSET TNearJmpSpliceRec = packed record JmpOpcode: Byte; Offset: DWORD; end; //         JMP NEAR OFFSET TNearJmpSpliceData = packed record FuncAddr: FARPROC; OldData: TNearJmpSpliceRec; NewData: TNearJmpSpliceRec; end; var NearJmpSpliceRec: TNearJmpSpliceData; //         procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); try //   !!! Move(NewData, FuncAddr^, SizeOf(TNearJmpSpliceRec)); finally VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, FuncAddr, SizeOf(TNearJmpSpliceRec)); end; end; //    MessageBoxA function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin //   SpliceNearJmp(NearJmpSpliceRec.FuncAddr, NearJmpSpliceRec.OldData); try //    S := AnsiString('Function interepted. Original message: ' + lpText); Result := MessageBoxA(Wnd, PAnsiChar(S), lpCaption, uType); finally //   SpliceNearJmp(NearJmpSpliceRec.FuncAddr, NearJmpSpliceRec.NewData); end; end; procedure InitNearJmpSpliceRec; begin //      NearJmpSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); //      ,     Move(NearJmpSpliceRec.FuncAddr^, NearJmpSpliceRec.OldData, SizeOf(TNearJmpSpliceRec)); //   JMP NEAR NearJmpSpliceRec.NewData.JmpOpcode := $E9; //    NearJmpSpliceRec.NewData.Offset := PAnsiChar(@InterceptedMessageBoxA) - PAnsiChar(NearJmpSpliceRec.FuncAddr) - SizeOf(TNearJmpSpliceRec); end; procedure TForm1.FormCreate(Sender: TObject); begin //     InitNearJmpSpliceRec; //  MessageBoxA SpliceNearJmp(NearJmpSpliceRec.FuncAddr, NearJmpSpliceRec.NewData); end; procedure TForm1.Button1Click(Sender: TObject); begin MessageBoxA(0, 'Test MessageBoxA Message', nil, 0); end; end. 

TNearJmpSpliceRec. .
TNearJmpSpliceData, .

InitNearJmpSpliceRec , SpliceNearJmp .

InterceptedMessageBoxA .

, PUSH ADDR + RET TNearJmpSpliceRec InitNearJmpSpliceRec, .

( HotPatch).
, , .

 unit CommonHotPatch; interface uses Windows; const LOCK_JMP_OPKODE: Word = $F9EB; type //      JMP NEAR OFFSET TNearJmpSpliceRec = packed record JmpOpcode: Byte; Offset: DWORD; end; THotPachSpliceData = packed record FuncAddr: FARPROC; SpliceRec: TNearJmpSpliceRec; LockJmp: Word; end; var HotPathSpliceRec: THotPachSpliceData; procedure InitHotPatchSpliceRec; procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word); implementation //         procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); try Move(NewData, FuncAddr^, SizeOf(TNearJmpSpliceRec)); finally VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, FuncAddr, SizeOf(TNearJmpSpliceRec)); end; end; //         procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, 2, PAGE_EXECUTE_READWRITE, OldProtect); try asm mov ax, NewData mov ecx, FuncAddr lock xchg word ptr [ecx], ax end; finally VirtualProtect(FuncAddr, 2, OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, FuncAddr, 2); end; end; function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); try //    S := AnsiString('Function interepted. Original message: ' + lpText); Result := MessageBoxA(Wnd, PAnsiChar(S), lpCaption, uType); finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; end; procedure InitHotPatchSpliceRec; begin //      HotPathSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); //      ,     Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, 2); //   JMP NEAR HotPathSpliceRec.SpliceRec.JmpOpcode := $E9; //    HotPathSpliceRec.SpliceRec.Offset := PAnsiChar(@InterceptedMessageBoxA) + 5 - PAnsiChar(HotPathSpliceRec.FuncAddr) - SizeOf(TNearJmpSpliceRec); end; end. 

:

 unit uHotPachSplice; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation uses CommonHotPatch; {$R *.dfm} procedure TForm1.FormCreate(Sender: TObject); begin //     InitHotPatchSpliceRec; //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - 5, HotPathSpliceRec.SpliceRec); //  MessageBoxW SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; procedure TForm1.Button1Click(Sender: TObject); const TestStr: AnsiString = 'Test MessageBoxA Message'; begin MessageBoxA(0, PAnsiChar(TestStr), nil, 0); end; end. 

, .. .

8. .


, .

, — VirtualAllocEx ( ), , .

, , .

:

 program hook_loader; {$APPTYPE CONSOLE} uses Windows; var hLib: THandle; HookProcAddr: Pointer; HookHandle: HHOOK; begin hLib := LoadLibrary('hook_splice_lib.dll'); try HookProcAddr := GetProcAddress(hLib, 'HookProc'); Writeln('MessageBoxA intercepted, press ENTER to resume...'); HookHandle := SetWindowsHookEx(WH_GETMESSAGE, HookProcAddr, hLib, 0); Readln; UnhookWindowsHookEx(HookHandle); finally FreeLibrary(hLib); end; end. 

SetWindowsHookEx , hook_splice_lib.dll. GetMessage PeekMessage.

:

 library hook_splice_lib; uses Windows, CommonHotPatch in '..\common\CommonHotPatch.pas'; {$R *.res} procedure DLLEntryPoint(dwReason: DWORD); begin case dwReason of DLL_PROCESS_ATTACH: begin //     InitHotPatchSpliceRec; //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - 5, HotPathSpliceRec.SpliceRec); //  MessageBoxW SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; DLL_PROCESS_DETACH: begin //      SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); end; end; end; function HookProc(Code: Integer; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall; begin Result := CallNextHookEx(0, Code, WParam, LParam); end; exports HookProc; begin DLLProc := @DLLEntryPoint; DLLEntryPoint(DLL_PROCESS_ATTACH); end. 

. , HookProc, . WH_GETMESSAGE.

DLLEntryPoint DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH .

DLLProc. , DLL_PROCESS_DETACH. , , DLL_PROCESS_ATTACH DLLProc. DLLEntryPoint(DLL_PROCESS_ATTACH) . , DLLProc DLLEntryPoint , .

9. .


. , . , , , . :

 program test_console8; {$APPTYPE CONSOLE} uses Windows; begin Writeln('Press enter to show message...'); Readln; MessageBoxA(0, 'First message', '', 0); Writeln('Press enter to show message...'); Readln; MessageBoxA(0, 'Second message', '', 0); end. 

GetMessage PeekMessage, .

, , , . MessageBox ( ) ( ) , MessageBox.

( MessageBox ), , (PID).

, , HookProc :

 procedure SelfUnload(lpParametr: Pointer); stdcall; begin FreeLibraryAndExitThread(HInstance, 0); end; exports SelfUnload; 

:

 const DllName = 'thread_splice_lib.dll'; function InjectLib(ProcessID: Integer): Boolean; var Process: HWND; ThreadRtn: FARPROC; DllPath: AnsiString; RemoteDll: Pointer; BytesWriten: DWORD; Thread: DWORD; ThreadId: DWORD; begin Result := False; //   Process := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_OPERATION or PROCESS_VM_WRITE, True, ProcessID); if Process = 0 then Exit; try //       DllPath := AnsiString(ExtractFilePath(ParamStr(0)) + DLLName) + #0; RemoteDll := VirtualAllocEx(Process, nil, Length(DllPath), MEM_COMMIT or MEM_TOP_DOWN, PAGE_READWRITE); if RemoteDll = nil then Exit; try //         if not WriteProcessMemory(Process, RemoteDll, PChar(DllPath), Length(DllPath), BytesWriten) then Exit; if BytesWriten <> DWORD(Length(DllPath)) then Exit; //     Kernel32.dll ThreadRtn := GetProcAddress(GetModuleHandle('Kernel32.dll'), 'LoadLibraryA'); if ThreadRtn = nil then Exit; //    Thread := CreateRemoteThread(Process, nil, 0, ThreadRtn, RemoteDll, 0, ThreadId); if Thread = 0 then Exit; try //     ... Result := WaitForSingleObject(Thread, INFINITE) = WAIT_OBJECT_0; finally CloseHandle(Thread); end; finally VirtualFreeEx(Process, RemoteDll, 0, MEM_RELEASE); end; finally CloseHandle(Process); end; end; 

, .
:

 function ResumeLib(ProcessID: Integer): Boolean; var hLibHandle: THandle; hModuleSnap: THandle; ModuleEntry: TModuleEntry32; OpCodeData: Word; Process: HWND; BytesWriten: DWORD; Thread: DWORD; ThreadId: DWORD; ExitCode: DWORD; PLibHandle: PDWORD; OpCode: PWORD; CurrUnloadAddrOffset: DWORD; UnloadAddrOffset: DWORD; begin Result := False; //          hLibHandle := LoadLibrary(PChar(DLLName)); try UnloadAddrOffset := DWORD(GetProcAddress(hLibHandle, 'SelfUnload')) - hLibHandle; if UnloadAddrOffset = -hLibHandle then Exit; finally FreeLibrary(hLibHandle); end; //        hModuleSnap := CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessID); if hModuleSnap <> INVALID_HANDLE_VALUE then try FillChar(ModuleEntry, SizeOf(TModuleEntry32), #0); ModuleEntry.dwSize := SizeOf(TModuleEntry32); if not Module32First(hModuleSnap, ModuleEntry) then Exit; repeat if AnsiUpperCase(ModuleEntry.szModule) = AnsiUpperCase(DLLName) then begin //     CurrUnloadAddrOffset := ModuleEntry.hModule + UnloadAddrOffset; Break; end; until not Module32Next(hModuleSnap, ModuleEntry); finally CloseHandle(hModuleSnap); end; //   Process := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_OPERATION or PROCESS_VM_WRITE, True, ProcessID); if Process = 0 then Exit; try //   jmp [ebx] OpCode := VirtualAllocEx(Process, nil, 2, MEM_COMMIT or MEM_TOP_DOWN, PAGE_READWRITE); if OpCode = nil then Exit; try OpCodeData := $23FF; if not WriteProcessMemory(Process, OpCode, @OpCodeData, 2, BytesWriten) then Exit; //     (    EBX   ) PLibHandle := VirtualAllocEx(Process, nil, 4, MEM_COMMIT or MEM_TOP_DOWN, PAGE_READWRITE); if PLibHandle = nil then Exit; try if not WriteProcessMemory(Process, PLibHandle, @CurrUnloadAddrOffset, 4, BytesWriten) then Exit; //   Thread := CreateRemoteThread(Process, nil, 0, OpCode, PLibHandle, 0, ThreadId); if Thread = 0 then Exit; try //     ... if (WaitForSingleObject(Thread, INFINITE) = WAIT_OBJECT_0) then if GetExitCodeThread(Thread, ExitCode) then Result := ExitCode = 0; finally CloseHandle(Thread); end; finally VirtualFreeEx(Process, PLibHandle, 0, MEM_RELEASE); end; finally VirtualFreeEx(Process, OpCode, 0, MEM_RELEASE); end; finally CloseHandle(Process); end; end; 

c , Windows 2000 Windows 7 ( , ). , lpParameter CreateRemoteThread EBX . JMP [EBX] . EBX SelfUnload, , .

SelfUnload, , :)

10.


, .
, , .

, .


, «» , , ReadFile .

— , , , WriteFile , .

11.


. . — .. Delphi, , .. « ».

HotPatch, : , .
, , , , , .

, . , , . , , . , , ?



.

Alexander (Rouse_) Bagel
, 2013

Source: https://habr.com/ru/post/178393/


All Articles