📜 ⬆️ ⬇️

Proper splicing when intercepting functions prepared for HotPatch

In the last article I looked at five options for intercepting functions including their variations.

True, I left it unresolved two unpleasant situations:
1. The call to the intercepted function at the moment when the trap is removed.
2. Simultaneous call of the intercepted function from two different threads.

In the first case, the programmer who installed the interceptor will not see the whole picture, because part of the data will pass him.
The second case faces more serious consequences, including the crash of the application in which the interceptor is installed.
')
Both of these situations can only be in the case of splicing. When intercepted via import / export tables, etc. the body of the intercepted function is not modified; therefore, these variants of interception do not require excessive gestures.

This article will discuss in more detail the splicing of the entry point of a function prepared for HopPatch, since These functions provide us with a way to avoid the above errors.

Interception by splicing via JMP NEAR OFFSET or PUSH ADDR + RET (most vulnerable to these errors) will not be considered, since for good, without the implementation of the length disassembler, it is impossible to make this interception option work as it should.



1. We realize the application intercepting the call to CreateWindowExW


To begin with, we will prepare an application that will visually show us the loss of data when intercepting the API due to the fact that the call to the intercepted function can occur at the moment when the interception is removed from it.

Create a new project and place three elements on the main form: TMemo, TOpenDialog and TButton.

The essence of the application: when you click a button, the interception will be set to the CreateWindowExW function and a dialog will be displayed. After closing the dialog, TMemo will display information about all the windows created by the dialog.

To do this, we need a part of the code from the previous article , namely:

1. Declaration of types and constants for interception:

const LOCK_JMP_OPKODE: Word = $F9EB; JMP_OPKODE: Word = $E9; type //      JMP NEAR OFFSET TNearJmpSpliceRec = packed record JmpOpcode: Byte; Offset: DWORD; end; THotPachSpliceData = packed record FuncAddr: FARPROC; SpliceRec: TNearJmpSpliceRec; LockJmp: Word; end; const NearJmpSpliceRecSize = SizeOf(TNearJmpSpliceRec); LockJmpOpcodeSize = SizeOf(Word); 


2. Procedures for recording NEAR JMP and atomic recording SHORT JMP

 //         procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, NearJmpSpliceRecSize, PAGE_EXECUTE_READWRITE, OldProtect); try Move(NewData, FuncAddr^, NearJmpSpliceRecSize); finally VirtualProtect(FuncAddr, NearJmpSpliceRecSize, OldProtect, OldProtect); end; end; //         procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, LockJmpOpcodeSize, PAGE_EXECUTE_READWRITE, OldProtect); try asm mov ax, NewData mov ecx, FuncAddr lock xchg word ptr [ecx], ax end; finally VirtualProtect(FuncAddr, LockJmpOpcodeSize, OldProtect, OldProtect); end; end; 


3. A slightly modified procedure for initializing the structure of THotPachSpliceData

 //       procedure InitHotPatchSpliceRec(const LibraryName, FunctionName: string; InterceptHandler: Pointer; out HotPathSpliceRec: THotPachSpliceData); begin //      HotPathSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName)); //      ,     Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize); //   JMP NEAR HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE; //    (  NearJmpSpliceRecSize  , // ..     ) HotPathSpliceRec.SpliceRec.Offset := PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr); end; 


We will place all this code in a separate SpliceHelper module, we will need it in the following chapters.

Now let's move to the main form, we need two global variables:

 var HotPathSpliceRec: THotPachSpliceData; WindowList: TStringList; 


The variable HotPathSpliceRec will contain information about the interceptor. The second will contain a list of created windows.

In the form constructor, we will initialize the structure of THotPachSpliceData.

 procedure TForm1.FormCreate(Sender: TObject); begin //     InitHotPatchSpliceRec(user32, 'CreateWindowExW', @InterceptedCreateWindowExW, HotPathSpliceRec); //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize, HotPathSpliceRec.SpliceRec); end; 


Create a function interceptor, called instead of the original function.

 function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar; lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall; var S: string; Index: Integer; begin //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); try //      Index := -1; if not IsBadReadPtr(lpClassName, 1) then begin S := 'ClassName: ' + string(lpClassName); S := IntToStr(WindowList.Count + 1) + ': ' + S; Index := WindowList.Add(S); end; //    Result := CreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); //       if Index >= 0 then begin S := S + ', handle: ' + IntToStr(Result); WindowList[Index] := S; end; finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; end; 


And it remains to complete the implementation of the button handler.

 procedure TForm1.Button1Click(Sender: TObject); begin //  CreateWindowExW SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); try //           WindowList := TStringList.Create; try //   OpenDialog1.Execute; //      Memo1.Lines.Text := WindowList.Text; finally WindowList.Free; end; finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); end; end; 


Everything is ready, you can run the program for execution.

I will not talk in detail about the code implemented in this chapter, it is more than described in detail in the previous article , it makes no sense to paint a second time.

Start the program, click the button and close the dialog by pressing the button "Cancel", it should turn out like this:

image

Thus, we found that when you open a regular TOpenDialog, 14 windows of different classes are created.

Now let's find out if this is true.

2. Create an auxiliary utility for viewing the application window tree.


To check the operation of our interceptor, it is necessary to insure a third-party utility that can display an up-to-date list of windows from the application, with which we will find out whether we have received all the information with our interceptor or not.

You can of course use third-party programs, like Spy ++, but we are programmers, that we should implement it ourselves, all the more so is the time to implement it.

Create a new project and place on the main form TTreeView and then implement the following code:

 type TdlgWindowTree = class(TForm) WindowTreeView: TTreeView; procedure FormCreate(Sender: TObject); private procedure Sys_Windows_Tree(Node: TTreeNode; AHandle: HWND; ALevel: Integer); end; ... procedure TdlgWindowTree.FormCreate(Sender: TObject); begin Sys_Windows_Tree(nil, GetDesktopWindow, 0); end; procedure TdlgWindowTree.Sys_Windows_Tree(Node: TTreeNode; AHandle: HWND; ALevel: Integer); type TRootNodeData = record Node: TTreeNode; PID: Cardinal; end; var szClassName, szCaption, szLayoutName: array[0..MAXCHAR - 1] of Char; szFileName : array[0..MAX_PATH - 1] of Char; Result: String; PID, TID: Cardinal; I: Integer; RootItems: array of TRootNodeData; IsNew: Boolean; begin //      while AHandle <> 0 do begin //    GetClassName(AHandle, szClassName, MAXCHAR); //  ( Caption)  GetWindowText(AHandle, szCaption, MAXCHAR); //    if GetWindowModuleFilename(AHandle, szFileName, SizeOf(szFileName)) = 0 then FillChar(szFileName, 256, #0); TID := GetWindowThreadProcessId(AHandle, PID); //   AttachThreadInput(GetCurrentThreadId, TID, True); VerLanguageName(GetKeyboardLayout(TID) and $FFFF, szLayoutName, MAXCHAR); AttachThreadInput(GetCurrentThreadId, TID, False); //  Result := Format('%s [%s] Caption = %s, Handle = %d, Layout = %s', [String(szClassName), String(szFileName), String(szCaption), AHandle, String(szLayoutName)]); //       if ALevel in [0..1] then begin IsNew := True; for I := 0 to Length(RootItems) - 1 do if RootItems[I].PID = PID then begin Node := RootItems[I].Node; IsNew := False; Break; end; if IsNew then begin SetLength(RootItems, Length(RootItems) + 1); RootItems[Length(RootItems) - 1].PID := PID; RootItems[Length(RootItems) - 1].Node := WindowTreeView.Items.AddChild(nil, 'PID: ' + IntToStr(PID)); Node := RootItems[Length(RootItems) - 1].Node; end; end; //   Sys_Windows_Tree(WindowTreeView.Items.AddChild(Node, Result), GetWindow(AHandle, GW_CHILD), ALevel + 1); //   ( )  AHandle := GetNextWindow(AHandle, GW_HWNDNEXT); end; end; 


Actually everything can be run:

image

3. Analyzing the results


Now compare the results of the work of both programs. We do this as follows.
1. Start the program with the interceptor and click on the button displaying the dialog.
2. Run the utility from the second chapter.
3. Close the dialog of the first program, to get the result of the intercepted windows.

We look:

image

The red window with the class Auto-Suggest DropDown is highlighted, let's see what it is:

image

And it turns out it contains 4 more windows, two scrolbars, a ListView, which also holds SysHeader32 with the same child. But this is already interesting. Henly windows in both applications are the same, but neither ListView nor SysHeader32, we see even two scrolls in the first application.

But, the fact that we do not see them in the first list does not mean anything. Creation of these windows occurred at the moment when our interceptor was removed, and this could happen only for one reason - due to the fact that the call to CreateWindowExW can lead to a recursive call to itself.

So you need to implement the interceptor code in such a way that it does not require the removal and restoration of the interception.

4. Calling an intercepted function without removing the interception code.


Let's look at this picture from the previous article.

image

This shows the beginning of the MessageBoxW function. The very first instruction is the non-doing instruction MOV EDI, EDI, preceded by five instructions NOP.

This is exactly how most of the functions that are prepared for interception via HotPatch, including the intercepted by CreateWindowExW, look like.

In the case of intercepting a function, instead of the seven bytes allocated, occupied with instructions that do nothing, the following code will be located:

image

Actually this is the interceptor installed by us.
Instead of the instruction MOV EDI, EDI the code JMP-7 is placed, transferring control to the previous instruction.
Instead of five NOP instructions, the jump to the beginning of the interceptor function is located.

If we start executing not from the address of the beginning of the CreateWindowExW function, but from the address of its first useful instruction PUSH EBP, then we will not affect the interceptor installed by us, and if so, then there is no point in removing it.

In code form, it looks like this:

 type TCreateWindowExW = function(dwExStyle: DWORD; lpClassName: PWideChar; lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; AMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall; function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar; lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall; var S: string; Index: Integer; ACreateWindowExW: TCreateWindowExW; begin //      Index := -1; if not IsBadReadPtr(lpClassName, 1) then begin S := 'ClassName: ' + string(lpClassName); S := IntToStr(WindowList.Count + 1) + ': ' + S; Index := WindowList.Add(S); end; //    @ACreateWindowExW := PAnsiChar(HotPathSpliceRec.FuncAddr) + LockJmpOpcodeSize; Result := ACreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); //       if Index >= 0 then begin S := S + ', handle: ' + IntToStr(Result); WindowList[Index] := S; end; end; 


Having calculated the address of the first useful instruction equal to the offset from the beginning of the function by two bytes, we store it in the ACreateWindowExW time variable, and then call the function in the usual way.

Let's see what happens in this case, here we expect it:

image

And that is exactly what we find in the list issued to us:

image

Well, we have found our “losses”, all the same 26 windows are created by calling TOpenDialog, and not 14.

The whole thing was in the notorious recursive call, which can be seen in the procedure call stack, if you set the breakpoint at the beginning of the InterceptedCreateWindowExW function.

image

5. Error while calling intercepted function from different threads.


With this error, the same is simple. If we constantly remove and restore the interceptor function, then at some point we will get an error in the function SpliceLockJmp on the instruction "lock xchg word ptr [ecx], ax". The fact is that at this moment the operation of returning page attributes to an interceptor address from another thread can be completed and, despite the fact that we have allowed to write to this address in our thread, the actual page attributes will be completely different.

It is with this behavior that the author of this thread encountered: interception of recv .

This error should be solved in the same way as shown above.
However, one should not forget about the interception handler, it should also be ThreadSafe, but the implementation of the handler is up to you.

6. Is it always possible to skip the first two bytes of the function being intercepted?


An interesting question and the answer to it - no, not always.
When functions are prepared for interception using the HotPatch method, Microsoft only guarantees that there will always be five NOP instructions in front of them and each such function will begin with a two-byte instruction. More we are not guaranteed anything.

If you look at the MessageBoxW or CreateWindowExW code, you can see that their first useful instruction, PUSH EBP, is one byte. Thus, since it does not satisfy the conditions, the body of this function is preceded by an empty call MOV EDI, EDI. The same will be true for functions starting with instructions of length three and more bytes. However, if the function starts with two-byte instructions, it does not make sense to inflate its body with an empty cap, because all the conditions for HotPatch are met (five NOP and 2 bytes).

In this case, if we apply the method described above, we will see nothing but an error.

An example of such a function is RtlCreateUnicodeString.
It starts with a useful PUSH $ 0C instruction.

image

The simplest solution would be to restore the original instructions before calling the original function, but as I said from the very beginning, this could lead to errors.

Therefore, we were faced with the task of ensuring the call of the erased instruction and ensuring the operability of the function even with the interception code set:

image

In principle, we have the machine code of the wiped instruction and it is stored in the HotPathSpliceRec.LockJmp structure, but we cannot call it directly for several reasons.

Well, firstly, this structure is located in the heap (or rather, not in the heap, but in the allocated memory, since Delphi does not work with the Heap mechanism directly) which has no performance attributes, i.e. if we somehow execute CALL at HotPathSpliceRec.LockJmp, we get an error.

You can of course set the correct attributes of the page, but this is too clumsy, though the executable code should not be mixed up with the data area.

Secondly, even if we pass the execution to this instruction, we must force the JMP instruction to the correct address after it (in this case it will be $ 77B062FB, see the previous picture), taking into account the offset of the instruction being called.

Third, in addition to the call, we must place on the stack in the correct order the parameters passed to the called function, which at least will lead us to the need to use asm inserts.

Let's try to solve everything in order.

In order not to get involved in passing parameters from an ASM insert, we can implement a certain springboard function by assigning this task to the compiler.

Those. roughly write an interceptor like this:

 function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): Integer; stdcall; begin asm db $90, $90, $90, $90, $90, $90, $90 end; end; function InterceptedRtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): Integer; stdcall; begin Result := TrampolineRtlCreateUnicodeString(DestinationString, SourceString); ShowMessage(DestinationString^.Buffer); end; 


In this case, the interceptor will be engaged in the call springboard and logging.

Inside the springboard function, 7 bytes are reserved, which is just enough for us to write two-byte erased instructions and five-byte NEAR JMP.
The function itself is located in the code area, and there should be no difficulties with its call.

And now an important nuance.
If we write these 7 bytes in place of the reserved block, we will encounter one unpleasant feature of Delphi. The fact is that the Delphi compiler almost always generates a prolog and an epilog for functions.

For example, let's say after the patch, the code of our function began to look like this:

 function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): Integer; stdcall; begin asm push $0C //    jmp $77B062FB //      end; end; 


In fact, it will turn into the following:

image

Those. on the stack, instead of two parameters DestinationString and SourceString, the values ​​of the EBP and ECX registers will be placed, which will result in absolutely unpredictable consequences.

We absolutely do not need this, so we will proceed more simply, namely the springboard code will be written right from the beginning of this function, overwriting the instructions of the function prologue.

But in fact, in fact, we absolutely do not need these instructions, since after a jump into the body of the intercepted function and its execution, the control will not return to the springboard function distorted by our actions, but directly to the place from which it was called, i.e. in function - the interception handler.

Thus we implement the initialization of the interceptor in the following way:

 //            procedure InitHotPatchSpliceRecEx(const LibraryName, FunctionName: string; InterceptHandler, Trampoline: Pointer; out HotPathSpliceRec: THotPachSpliceData); var OldProtect: DWORD; TrampolineSplice: TNearJmpSpliceRec; begin //      HotPathSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName)); //      ,     Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize); //   VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize, PAGE_EXECUTE_READWRITE, OldProtect); try Move(HotPathSpliceRec.LockJmp, Trampoline^, LockJmpOpcodeSize); TrampolineSplice.JmpOpcode := JMP_OPKODE; TrampolineSplice.Offset := PAnsiChar(HotPathSpliceRec.FuncAddr) - PAnsiChar(Trampoline) - NearJmpSpliceRecSize; Trampoline := PAnsiChar(Trampoline) + LockJmpOpcodeSize; Move(TrampolineSplice, Trampoline^, SizeOf(TNearJmpSpliceRec)); finally VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize, OldProtect, OldProtect); end; //   JMP NEAR HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE; //    (  NearJmpSpliceRecSize  , // ..     ) HotPathSpliceRec.SpliceRec.Offset := PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr); end; 


The initialization itself and the call of the intercepted function look like this:

 type UNICODE_STRING = record Length: WORD; MaximumLength: WORD; Buffer: PWideChar; end; PUNICODE_STRING = ^UNICODE_STRING; function RtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): BOOLEAN; stdcall; external 'ntdll.dll'; ... procedure TForm2.FormCreate(Sender: TObject); begin //       InitHotPatchSpliceRecEx('ntdll.dll', 'RtlCreateUnicodeString', @InterceptedRtlCreateUnicodeString, @TrampolineRtlCreateUnicodeString, HotPathSpliceRec); //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize, HotPathSpliceRec.SpliceRec); end; procedure TForm2.Button1Click(Sender: TObject); var US: UNICODE_STRING; begin //  RtlCreateUnicodeString SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); try RtlCreateUnicodeString(@US, 'Test UNICODE String'); finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); end; end; 


Now you can click on the button and see the result of the interception as a message.

As a conclusion


As a result, the splice implementation variant shown in the sixth chapter is the most universal in the case of interception of functions prepared for HotPatch. It will work correctly in the case of a MOV EDI, EDI stub and if there is a useful instruction at the beginning of the function being intercepted. It is not subject to errors, described at the very beginning of the article, but the truth is to intercept the usual functions using this algorithm will not work, however, I already wrote about this earlier .

I apologize for having to split up the information into pieces and not give it all at once, but as I was advised a year ago, it’s better to give the material in small portions so that there is time for its digestion :)

On the other hand, if you collect all the material in a pile, then firstly it will take quite a long time, which I do not have available, and secondly it will lead to its unreadable due to the large volume (there were precedents).
Therefore, it is better that way.

The source code for the examples can be collected at this link .

© Alexander (Rouse_) Bagel
May, 2013

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


All Articles