Partager via


Detouring code

 

Microsoft Detours from Microsoft Research is a powerful technology to intercept operating system function calls and detour the call to your own code. This enables: 
1.    Diagnostics: you can log callers, parameters
2.    Replace functionality completely: the caller calls the WinAPI “MessageBox”, but it calls your version instead.
3.    Modify functionality: perhaps change some parameters to the call to change behavior.

The source code for Detours is https://github.com/Microsoft/Detours
Sample code below shows a window with a Button to invoke MessageBox, and a checkbox to toggle Detouring to a private implementation of MessageBox:
Start Visual Studio
File->New->Project->C++ Desktop Application. Call it "DetourSample"
Replace the entire DetourSample.cpp with this code.

Set a breakpoint on the call to MessageBoxA. Hit F5 to build and run, and don’t click the Detour Checkbox yet.
Click on the MessageBox button to reach the breakpoint.
Then hit Ctrl-F11 (Show Disassembly) to see the disassembly of the code in a few columns: First the address, then the machine code bytes (you may need to right-click->Show Code Bytes), then the Assembly code instructions and lastly the operands.

Notice that there are 4 push instructions, corresponding to the 4 parameters of MessageBoxA, in right to left order. Then comes the call to __imp__MessageBoxA@16
You can single step the assembly code and watch how the registers change values (Debug->Windows->Registers) (You may have to disable Tools->Options->Debugging->General->Just My Code)

                                  MessageBoxA(_hWnd, "Some Text", g_szArch, MB_OK);
013B9710 8B F4 mov esi,esp
013B9712 6A 00 push 0
013B9714 68 44 D0 3C 01 push offset g_szArch (013CD044h)
013B9719 68 60 A1 3C 01 push offset string "Some Text" (013CA160h)
013B971E 8B 45 08 mov eax,dword ptr [this]
013B9721 8B 88 94 01 00 00 mov ecx,dword ptr [eax+194h]
013B9727 51 push ecx
013B9728 FF 15 68 E1 3C 01 call dword ptr [__imp__MessageBoxA@16 (013CE168h)] 

Notice how the address of __imp_MessageBoxA@16 is 013CE168, and that the code bytes for that instruction are  “FF 15” + “68 E1 3C 01”  and the 2nd part is the address in Little Endian order (68 E1 3C 01 == 013CE168).
Debug->Windows->view->Memory 1 to show the memory window, then drag the “0x013CE168” to the Address of the window (you’ll need to right-click on the Memory Window and show as 4-byte integer)
This shows a portion of the import address table and the content of 013CE168 is 75b58830

image

Debug->Windows->Modules, and look for User32.dll. It shows the Address as 75AD0000-75C2F000, which includes our 75b58830,which is the entry point to User32!MessageBoxA

Another way to see this: from a Command prompt, run this to create a VS Developer prompt (adjust the path to your install path):
“C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\vc\Auxiliary\Build\vcvars32.bat”
Then type: link /dump /exports c:\windows\SysWow64\user32.dll | find “MessageBoxA”
This shows the user32.dll export of MessageBoxA , and the relative address. The last few hex digits of the result are “8830”
(Note: due to Windows Updates and different versions of windows, your numbers will probably be different from mine.)

When you step into the MessageBoxA function, you see the entire MessageBoxA function from User32.dll (it’s small because it just re-pushes the parameters and forwards to another function (notice the addition of a new parameter, 0)
Extra points if you can name that function

75B58830 8B FF mov edi,edi
75B58832 55 push ebp
75B58833 8B EC mov ebp,esp
75B58835 6A 00 push 0
75B58837 FF 75 14 push dword ptr [ebp+14h]
75B5883A FF 75 10 push dword ptr [ebp+10h]
75B5883D FF 75 0C push dword ptr [ebp+0Ch]
75B58840 FF 75 08 push dword ptr [ebp+8]
75B58843 E8 18 00 00 00 call 75B58860
75B58848 5D pop ebp
75B58849 C2 10 00 ret 10h 

 

For detours to work, the beginning of the MessageBoxA function must be replaced with a jmp instruction to the detoured function.
We can’t just replace instructions and expect the function to continue working: we need to still execute the replaced instructions. Where do we put them? User32.dll is laid out in memory exactly, and we can’t just squeeze in a few bytes. So we need to allocate some memory.

Click the Detour checkbox, then do the MessageBox again. The import address table hasn’t changed, so we still go to 75b58830:

75B58830 E9 43 88 85 8B jmp MyMessageBoxA (013B1078h)
75B58835 6A 00 push 0
75B58837 FF 75 14 push dword ptr [ebp+14h]
75B5883A FF 75 10 push dword ptr [ebp+10h]
75B5883D FF 75 0C push dword ptr [ebp+0Ch]
75B58840 FF 75 08 push dword ptr [ebp+8]
75B58843 E8 18 00 00 00 call 75B58860
75B58848 5D pop ebp
75B58849 C2 10 00 ret 10h 

Notice that there are 5 new bytes: a Long jmp instruction to the detoured version of MessageBoxA called MyMessageBoxA. The rest of the function is unchanged.
Those 5 new bytes replaced the first 3 original instructions:
75B58830 8B FF mov edi,edi
75B58832 55 push ebp
75B58833 8B EC mov ebp,esp 

 

The detoured version does its thing, then calls the real version via g_real_MessageBoxA, which now points to the memory allocated for the detour, which contains this:

image

 

And at the 6DB400D8 address we find the instructions that were replaced, plus a long jmp back to where we left off at 75B58835: (which is the instruction right after the detour jump)

6DB400D8 8B FF mov edi,edi
6DB400DA 55 push ebp
6DB400DB 8B EC mov ebp,esp
6DB400DD E9 53 87 01 08 jmp 75B58835 

So now we see why it’s called a detour: we took a side trip and came back to where we’re supposed to be.
Of course the detoured function can do all sorts of things, like:
1.    Change the parameters or return value before or after the call
2.    Not call the original function at all (could cause a crash)
3.    Record information about the function call.

Possible Problems:
1.    Suppose you’re in the middle of replacing the 5 bytes of a function, when another thread tries to execute that function when only 4 bytes have been replaced? It goes off into random memory somewhere and crashes. You could suspend threads and then patch the thread stacks for each detour. (VS used to do this)
2.    If the API you want to detour isn’t loaded in memory  yet, it can still be detoured. You can detour any function for which you know the function signature and address. You can get the address via calling LoadLibrary, GetProcAddress. Even if you don’t know the signature, if you know the parameter sizes, and calling convention, you can detour the function successfully.
3.    Suppose you want to detour from various component DLLs in a single app. Each detour will allocate its own memory to store the detours. Because each memory region needs to have certain rights, each region is allocated via VirtualAlloc, which uses a minimum 64k for a 32 bit process. That means if 3 DLLs are detouring, that’s using 3 * 64K = 192K Virtual Address space. Visual Studio used to do this.

Possible Expansions:
1.    Put your detour code into a DLL, then inject that DLL into another process. MemSpect does this.
2.    Have your detoured functions collect information and communicate that data to another process so you can “spy” on the target process. MemSpect does this.
3.    Combine detours with CLR Profiler APIs. Guess what?  MemSpect does this too.

 

<Code Sample>

 // DetourSample.cpp : Defines the entry point for the application.
//
 
#include "stdafx.h"
#include "DetourSample.h"
#include <string>
/*
Start Visual Studio
File->New->Project->C++ Desktop Application. Call it "DetourSample"
Replace the entire DetourSample.cpp with this code.
 
From https://github.com/Microsoft/Detours copy 4 files: detours.h, detours.cpp, disasm.cpp, Modules.cpp next to this file
Include the 3 .cpp files in the project (Solution Explorer->Show all files->Right click->Include in Project)
Need to turn off precompiled headers for all configurations (both debug/release, 32 bit, 64 bit). Project->Properties->Configuration->C/C++->PrecompiledHeaders->Not using PrecompiledHeaders
Many C++ projects consist of lots of C++ files, which have many #include at the top. Each #include file needs to be expanded and compiled for each C++ file.
The PrecompiledHeaders option specifies an included file name like "windows.h". Every C++ file is assumed to be identical up to the line that includes "windows.h"
This allows that identical part to be precompiled into a .pch file on disk, which is only needed to be done once for all the C++ files.
Works in 64 bit and 32 bit: Build->Configuration->Active Solution Platform->x86 or x64
 
 
*/
#ifdef _WIN64
char g_szArch[] = "x64 64 bit";
#define _X86_
#else
char g_szArch[] = "x86 32 bit";
#endif
#include "detours.h"
 
using namespace std;
 
#define MAX_LOADSTRING 100
 
class CMyApplication;
CMyApplication * g_CMyApplication;
 
// Forward declarations of functions included in this code module:
LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM, LPARAM);
 
/*
Many windows API functions that require a string parameter have 2 versions: an A (for ANSI) and W (for Wide Unicode) strings.
For example, MessageBoxA and MessageBoxW.
If you just call MessageBox, the relevant header (WinUser.h) contains:
#ifdef UNICODE
#define MessageBox  MessageBoxW
#else
#define MessageBox  MessageBoxA
#endif // !UNICODE
 
Detouring the W version might detour both the W and the A version because the A version might just convert to W and call the W version.
That's not true with MessageBoxA and MessageBoxW. MessageBoxA on Win10 converts to unicode and then calls (undocumented) MessageBoxTimeOutW
We can look at the exports of user32.dll from a VS command prompt:
C:\>link /dump /exports \Windows\System32\user32.dll | find /i "messagebo"
2149  282 0006F000 MessageBoxA
2150  283 0006F060 MessageBoxExA
2151  284 0006F090 MessageBoxExW
2152  285 0006F0C0 MessageBoxIndirectA
2153  286 0006F260 MessageBoxIndirectW
2154  287 0006F320 MessageBoxTimeoutA
2155  288 0006F470 MessageBoxTimeoutW
2156  289 0006F640 MessageBoxW
2408  387 0006F6A0 SoftModalMessageBox
*/
 
 
// decltype is like typeof()
// initialize a pointer to the real version of the API
decltype(&MessageBoxA) g_real_MessageBoxA = &MessageBoxA;
decltype(&MessageBoxW) g_real_MessageBoxW = &MessageBoxW;
 
// here is my implementation that will be called when detoured.
int WINAPI MyMessageBoxA(
   _In_opt_ HWND hWnd,
    _In_opt_ LPCSTR lpText,
    _In_opt_ LPCSTR lpCaption,
 _In_ UINT uType)
{
  string caption(lpCaption);
 caption += " The detoured version of MessageboxA";
   string text(lpText);
   text += " hi from detoured version";
 // we now call the real implementation of MessageBox.
  // (we could log various things about detoured API calls, such as
  //   the callstack and the return value)
   return g_real_MessageBoxA(hWnd, text.c_str(), caption.c_str(), uType);
}
 
int WINAPI MyMessageBoxW(
 _In_opt_ HWND hWnd,
    _In_opt_ LPCWSTR lpText,
   _In_opt_ LPCWSTR lpCaption,
    _In_ UINT uType)
{
  wstring caption(lpCaption);
    caption += L" The unicode detoured version of MessageboxA";
  wstring text(lpText);
  text += L" hi from unicode detoured version";
    return g_real_MessageBoxW(hWnd, text.c_str(), caption.c_str(), uType);
}
 
 
class CMyApplication
{
 WCHAR _szTitle[MAX_LOADSTRING];                  // The title bar text
   WCHAR _szWindowClass[MAX_LOADSTRING];            // the main window class name
 HINSTANCE _hInstance;                                // current instance
  HWND _hWnd;
    HWND _hwndBtnMsgBox;
   HWND _hwndChkDetour;
   POINTS _size;
public:
   CMyApplication(HINSTANCE hInstance)
    {
       _hInstance = hInstance;
       // Initialize global strings
       LoadStringW(_hInstance, IDS_APP_TITLE, _szTitle, MAX_LOADSTRING);
     LoadStringW(_hInstance, IDC_DETOURSAMPLE, _szWindowClass, MAX_LOADSTRING);
 
       WNDCLASSEXW wcex;
      wcex.cbSize = sizeof(WNDCLASSEX);
 
        wcex.style = CS_HREDRAW | CS_VREDRAW;
       wcex.lpfnWndProc = WindowProcedure;
       wcex.cbClsExtra = 0;
      wcex.cbWndExtra = 0;
      wcex.hInstance = hInstance;
       wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_DETOURSAMPLE));
     wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
       wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_DETOURSAMPLE);
       wcex.lpszClassName = _szWindowClass;
      wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
     RegisterClassExW(&wcex);
    }
 
  void Initialize()
  {
       _hWnd = CreateWindowW(_szWindowClass, _szTitle, WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, _hInstance, nullptr);
 
     MoveWindow(_hWnd, 400, 400, 700, 400, /*bRepaint*/ false);
       ShowWindow(_hWnd, SW_NORMAL);
      UpdateWindow(_hWnd);
        ShowStatusMsg(L"Click to show MessageBox, RightClick to toggle detours (%S)", g_szArch);
      _hwndBtnMsgBox = CreateWindow(
            L"BUTTON",  // Predefined class; Unicode assumed 
          L"MsgBox",      // Button text 
            WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,  // Styles 
            0,         // x position 
            0,         // y position 
            100,        // Button width
            20,        // Button height
            _hWnd,     // Parent window
           NULL,       // No menu.
          _hInstance,
         NULL);      // Pointer not needed.
 
       _hwndChkDetour = CreateWindow(
            L"BUTTON",  // Predefined class; Unicode assumed 
          L"Detour",      // Button text 
            WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_AUTOCHECKBOX,  // Styles 
         130,         // x position 
          0,         // y position 
            100,        // Button width
            20,        // Button height
            _hWnd,     // Parent window
           NULL,       // No menu.
          _hInstance,
         NULL);      // Pointer not needed.
    }
 
  int _topStatline = 25;
   int _statLine = _topStatline;
    void ShowStatusMsg(LPCWSTR pText, ...)
   {
       if (_hWnd != NULL)
        {
           va_list args;
           va_start(args, pText);
         wstring strtemp(1000, '\0');
          _vsnwprintf_s(&strtemp[0], 1000, _TRUNCATE, pText, args);
            va_end(args);
           auto len = wcslen(strtemp.c_str());
          strtemp.resize(len);
            HDC hDC = GetDC(_hWnd);
          HFONT hFont = (HFONT)GetStockObject(ANSI_FIXED_FONT);
            HFONT hOldFont = (HFONT)SelectObject(hDC, hFont);
           TEXTMETRIC textMetric;
         if (GetTextMetrics(hDC, &textMetric) && textMetric.tmAveCharWidth > 0)
         {
               // pad to full line to erase any prior content
             auto nCharsOnLine = _size.x / textMetric.tmAveCharWidth;
               if (nCharsOnLine > 0 && nCharsOnLine >= (int)strtemp.size())
               {
                   auto nCharsToPad = nCharsOnLine - strtemp.size();
                  if (nCharsToPad > 0)
                  {
                       strtemp.insert(strtemp.size(), nCharsToPad, ' ');
                    }
                   TextOut(hDC, 0, _statLine, strtemp.c_str(), (int)strtemp.size());
                   _statLine += textMetric.tmHeight;
                 if (_statLine > _size.y)
                  {
                       _statLine = _topStatline;
                 }
               }
           }
           SelectObject(hDC, hOldFont);
           ReleaseDC(_hWnd, hDC);
     }
   }
 
  bool _fIsDetouring = false;
  // Macro to show error. The expression is Stringized using the "#" operator.
 // the "%S" is used (upper case S) to convert to Unicode
#define IFFAILSHOWERROR(expr) \
 if ((err = expr) != NO_ERROR) \
   {\
      g_CMyApplication->ShowStatusMsg(L"Error %d %S", err, #expr); \
   }\
 
 void DoDetours()
   {
       int err;
       IFFAILSHOWERROR(DetourTransactionBegin());
       if (!_fIsDetouring)
        {
           ShowStatusMsg(L"Start Detouring");
         IFFAILSHOWERROR(DetourAttach((PVOID *)&g_real_MessageBoxA, MyMessageBoxA));
            IFFAILSHOWERROR(DetourAttach((PVOID *)&g_real_MessageBoxW, MyMessageBoxW));
        }
       else
        {
           ShowStatusMsg(L"Stop  Detouring");
         IFFAILSHOWERROR(DetourDetach((PVOID *)&g_real_MessageBoxA, MyMessageBoxA));
            IFFAILSHOWERROR(DetourDetach((PVOID *)&g_real_MessageBoxW, MyMessageBoxW));
        }
       IFFAILSHOWERROR(DetourTransactionCommit());
      _fIsDetouring = !_fIsDetouring;
   }
   LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
       switch (message)
       {
       case WM_COMMAND:
       {
           int wmId = LOWORD(wParam);
           // Parse the menu selections:
          switch (wmId)
          {
           case BN_CLICKED:
               if ((HWND)lParam == _hwndBtnMsgBox)
              {
                   MessageBoxA(_hWnd, "Some Text", g_szArch, MB_OK);
                   //            MessageBoxW(_hWnd, _T("Some Unicode Text"), _T("Caption"), MB_OK);
               }
               else if ((HWND)lParam == _hwndChkDetour)
                {
                   DoDetours();
                }
               break;
          case IDM_EXIT:
             DestroyWindow(hWnd);
                break;
          default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
       }
       break;
      case WM_SIZE:
      {
           auto pts = MAKEPOINTS(lParam);
           ShowStatusMsg(L"Size  %5d %5d", pts.x, pts.y);
           _size = pts;
      }
       break;
      case WM_PAINT:
     {
           PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);
            EndPaint(hWnd, &ps);
       }
       break;
      case WM_DESTROY:
           PostQuitMessage(0);
         break;
      default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
       return 0;
  }
 
  int DoMessageLoop()
    {
       HACCEL hAccelTable = LoadAccelerators(_hInstance, MAKEINTRESOURCE(IDC_DETOURSAMPLE));
       MSG msg;
       // Main message loop:
      while (GetMessage(&msg, nullptr, 0, 0))
     {
           if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
          {
               TranslateMessage(&msg);
             DispatchMessage(&msg);
          }
       }
       return (int)msg.wParam;
    }
};
 
LRESULT CALLBACK WindowProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
   return g_CMyApplication->WndProc(hWnd, message, wParam, lParam);
}
 
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
 _In_opt_ HINSTANCE hPrevInstance,
  _In_ LPWSTR    lpCmdLine,
   _In_ int       nCmdShow)
{
    // create an instance of CMyApplication  on stack
  CMyApplication aCMyApplication(hInstance);
 g_CMyApplication = &aCMyApplication;
  g_CMyApplication->Initialize();
 
 return g_CMyApplication->DoMessageLoop();
}

 

</Code Sample>