Professional Documents
Culture Documents
Function Hooking and Windows DLL Injection PDF
Function Hooking and Windows DLL Injection PDF
1/15
At first the Instruction Pointer is at offset x and execution goes through the jump, next it's going to be at offset y, running whatever lies there. The operand to this form of jmp is a 32 bit offset, a distance between the "landing point" and the "jump point". It can be negative. So we might run into less trouble by writing jmp distance between bar and (foo + 5) immediately after the entry point of foo().
2/15
26-11-2011
// by default they are only readable and executable BOOL res = VirtualProtect(foo, SIZE_PATCH, PAGE_EXECUTE_READWRITE, &oldProtection); if (!res) return 1; // be mindful of pointer arithmetic // works with PBYTE, won't with PDWORD DWORD distanceToNewFoo = bar - foo - SIZE_PATCH; *foo = OP_JMP; *reinterpret_cast<PDWORD>(foo + 1) = distanceToNewFoo; // called though the pointer instead of foo() // to make sure the compiler won't inline or do some other stupid stuff reinterpret_cast<MyProc>(foo)(); // will print "bar\n" return 0; }
An important thing to have in mind here, is that the conversion between function pointer and void * (PVOID, PBYTE, etc) is illegal in both C and C++3), even though it works in Visual C and in GCC. For Linux check mprotect() instead of VirtualProtect(). code as data The following image is an attempt at showing a map of the code from hotpatch.cpp. Keep in mind that the memory addresses from the left side are fictional.
http://elf.cs.pub.ro/so/wiki/
26-11-2011
3/15
1. foo() is 02114B05 - 011A3518 = 00F715ED bytes long, fits an unsigned char[16193005]. bar() is 0852AB24 Sisteme de Operare - http://elf.cs.pub.ro/so/wiki/
4/15
26-11-2011
- 0852AA02 = 00000122 bytes long, fits an unsigned char[290]. 2. the idea is to inject an unconditional jump at the beginning of foo(), jump that will land at the first instruction of bar(). 3. notice the distance between the expected "current" instruction within foo() at address 011A351D and the "landing zone" in bar(), address 0852AA02. We're talking about 0852AA02 - 011A351D = 073874E5 bytes across. 4. the E9 jump receives an immediate operand; that means the operand is within the code and the whole thing eats up 5 bytes, 1 for the opcode and 4 for the operand. That's why the execution jumps from 011A351D and not directly from the beginning of foo(), at 011A3518. As an observation, both statement n and statement m are far rets, opcode = CB. For the example in the picture, the program would write E9, E5, 74, 38, 07 just at the beginning of foo().
G G
E9 is the opcode for far jmp. A far jmp receives an immediate 32 bit argument. E5, 74, 38, 07 represent - due to endianness - the number 073874E5, the argument.
As a rule, it's better to make sure the new function matches the old one in both calling convention and return value + arguments.
Dll Injection
Now why would I go through the lengths of such useless (but fun) trickery? I won't answer in this section, but it's required to fully grasp what will follow. Under the Windows operating system, dll injection refers to executing foreign code in a different process. Ever wondered how applications such as Fraps4) or RivaTuner5) / MSI Aterburner6) manage to display a framerate counter on top of the currently running, full screen and exclusive mode D3D / OpenGL application? Or how, something like Sockscap7) manages to redirect an application's traffic through the given proxy? Read on. While the above applications take advantage of dll injection, keep in mind that the "global hotkeys" functionality from programs such as foobar20008) or AutoHotkey9) are implemented by means of a "cousin" method - via low level keyboard hooks. This is how one would run their code in a "parasited" process10): 1. with SetWindowsHookEx(). Installing global hooks, from dlls defining them, is dll injection at work. It's an OK method, explained here. 2. with CreateRemoteThread(LoadLibrary()). This is the true dll injection. 3. with WriteProcessMemory(OurThreadProc); CreateRemoteThread(OurThreadProc). This is so much in the true spirit of dll injection, that it's not even dll injection anymore, it's directly code injection! Here's the recipe for setting up a global Windows hook and getting your code into victim process: 1. think of a suitable criteria; for example when a window gets created. That type of event is called a shell event and it's hookable via SetWindowsHookEx(WH_SHELL, ShellProc) 2. write myhook.dll that defines and exports the above ShellProc(), see some steps below. For convenience, also export something like InstallHook() from that dll, and within the code add the call to SetWindowsHookEx(). 3. write a "launcher" program, that will call once the InstallHook() function defined in our dll and then just wait (if it unloads, probably Windows will also unload the dll, unless special measures are taken, resulting in crashes) 4. ShellProc() will get called by the system everytime the specific event - a shell event in our case - is to be received by the application. Its code will run in that application's address space! Watch the following picture if you don't believe:
http://elf.cs.pub.ro/so/wiki/
26-11-2011
5/15
1. normally, an event is dispatched to the message loop of the receiving thread, and the message loop further sends that message within the thread to a "proper" handler 2. myhook.dll registers, system-wide, ShellProc() as an interceptor of shell events (out of which WM_CREATE). Now the message goes through the code from ShellProc() and only after processing it's (not even necessary, only because I'm too kind) forwarded to the shell handler in the original event loop.
A Practical Example
I like playing my games in windowed mode. Inspired by Media Player Classic Homecinema's11) borderless always on top Sisteme de Operare - http://elf.cs.pub.ro/so/wiki/
6/15
26-11-2011
window mode, I thought to myself how cool would it be to have any game's window borderless and always on top of the other windows. A game's window usually contains either WS_CAPTION or WS_THICKFRAME window styles. Making it borderless is a simple matter of retrieving its style (GetWindowLong(GWL_STYLE)), removing (toggling), by means of XOR, the relevant style and then applying the result on the window handle SetWindowLong(GWL_STYLE).
Borderless1.cpp
#include <cstdio> using namespace std; #include <windows.h> int main() { bool done = false; // finish when 'q' is entered while (!done) { HWND hwnd = FindWindow(NULL, TEXT("Heroes of Newerth")); if (NULL != hwnd) { LONG styles = GetWindowLong(hwnd, GWL_STYLE); if (0 != styles) { styles ^= WS_CAPTION; LONG result = SetWindowLong(hwnd, GWL_STYLE, styles); } } char c; scanf("%c", &c); if ('q' == c) done = true; } return 0; }
http://elf.cs.pub.ro/so/wiki/
26-11-2011
7/15
You can observe 2 problems in the current approach: 1. the window is now blurry 2. the mouse hover effect is registered a few pixels off The blurriness problem happens because after removing the decorations (titlebar, dialog borders), Windows tries to maintain the program window within the same bounding rectangle. In doing so, it "inflates" the client area by a few pixels in each direction, pixels obtained from removing said decorations. Indeed, counting the pixels from the first picture, we get a total window area (including borders) of 1616858 and a client area (only the Direct3D part from within the borders where the application draws its updates) of 1600820 pixels. For the second picture, we have a window area equal to the client area (no more borders) of 1606848 pixels. Since Heroes of Newerth has a dialog frame (non-sizeable), it doesn't treat WM_SIZE messages; the Direct3D context keeps rendering at the initial config file resolution and the resulting frame buffer gets drawn, with stretching, on top of a sligthly different window area so we don't have a perfect "(logical )pixel-on-(screen )pixel" mapping anymore. Note my config file for the game:
//snip SetSave "vid_resolution" "1600,820" //snip
Here's a fix for keeping the client area of the window in place during the style change: Borderless2.cpp
#include <cstdio> using namespace std; #include <windows.h> int main() { bool done = false; while (!done) { HWND hwnd = FindWindow(NULL, TEXT("Heroes of Newerth")); if (NULL != hwnd) { LONG styles = GetWindowLong(hwnd, GWL_STYLE); if (0 != styles) { styles ^= WS_CAPTION; bool deflate = 0 == (styles & WS_CAPTION); RECT rc; GetWindowRect(hwnd, &rc); int captionHeight = GetSystemMetrics(SM_CYCAPTION); int borderWidth = GetSystemMetrics(SM_CXDLGFRAME); int borderHeight = GetSystemMetrics(SM_CYDLGFRAME);
8/15
26-11-2011
if (deflate) { rc.left += borderWidth; rc.right -= borderWidth; rc.top += captionHeight + borderHeight; rc.bottom -= borderHeight; } else { rc.left -= borderWidth; rc.right += borderWidth; rc.top -= captionHeight + borderHeight; rc.bottom += borderHeight; } LONG result = SetWindowLong(hwnd, GWL_STYLE, styles); if (0 != result) SetWindowPos(hwnd, NULL, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, SWP_FRAMECHANGED); } } char c; scanf("%c", &c); if ('q' == c) done = true; } return 0; }
A process such as Starcraft 2 owns a window with the WS_THICKFRAME style denoting a sizing border. If I change the code to remove that style from the Starcraft 2 main window (also SM_C?THICKFRAME instead of SM_C?DLGFRAME, I can only notice the mouse hover effect being off - the game catches WM_SIZE messages and adjusts the framebuffer accordingly.
What the game does when handling mouse input is obtain some absolute screen coordinates from DirectInput, convert them to window coordinates, then compensate for the window title and borders and then figure out what to do next with http://elf.cs.pub.ro/so/wiki/ Printed on 2011/11/26 07:59
26-11-2011
9/15
the event. Note that after removing the window decorations, both GetWindowRect() and GetClientRect() return the same coordinates. Then the game compensates by subtracting the expected widths and heights of borders, resulting in slightly off readings for the events. The idea here is to modify at run-time GetWindowRect() so that it returns the old rectangle, with titlebar and borders, as if the window style still contained those elements. The basic workflow for detouring GetWindowRect() is: 1. 2. 3. 4. 5. 6. insert exactly at the beginning of the function a jmp to our GetWindowRect(); hot patch it in our GetWindowRect() restore the code of the original function call the original function and save the result modify that result so that it fits our agenda hot patch GetWindowRect() again return the now modified result
Hot Patching
While the previous approach for hot patching a function (via relative jmp) is generally OK even in 64 bit mode, there could be rare occasions when the distance between the patched function and the original one can't be expressed on only 32 bits. Since the specific form of jmp used expects a 32 bit displacement, it would be useless in this case. The suggested solution here is a 64 bit absolute jump, which comes by default with x86-6412). The idea here is just slightly different than in the case of plain x86. While jmp accepted there as argument an immediate displacement, the x86-64 long jump functions through an indirect register. So we'd simply load the destination address (not a "distance" any more for this instruction) into a register and pass that register as the operand to the jmp. Fortunately, x86-64 states that the special 64-bit registries r10 and r11 aren't preserved between call opcodes, so picking r11 is a safe bet and guaranteed not to ruin the program flow.
Dll Injection
One can't insert an image compiled for 32 bits inside a 64 bit process and can't inject a 64 bit dll inside a 32 bit process. The idea is to have 2 separate programs making the dll injection, 2 versions of the dll, each of them for 64 bits and, respectively, for 32 bits. The 32 bit injector should set up the hooks as usual and launch a light weight version of itself that only sets up the hook and waits for an end event. Loader:
TCHAR path[MAX_PATH]; DWORD len = GetModuleFileName(NULL, path, MAX_PATH); while (path[len - 1] != '\') --len; MoveMemory(path + len, TEXT("Deframe64.dat"), sizeof(TCHAR) * (lstrlen(TEXT("Deframe64.dat")) + 1)); STARTUPINFO siDeframe64 = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION piDeframe64; BOOL bRes = CreateProcess(0, path, NULL, NULL, TRUE, 0, NULL, NULL, &siDeframe64, &piDeframe64); if (bRes) WaitForInputIdle(piDeframe64.hProcess, INFINITE); InstallHook();
Deframe64.cpp
#include <windows.h> #include "DeframeHelper.h" int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { InstallHook(); // main message loop: MSG msg; while (GetMessage(&msg, NULL, 0, 0) && WM_QUIT != msg.message) { TranslateMessage(&msg); DispatchMessage(&msg); }
10/15
26-11-2011
The first fragment of code, residing in the normal 32 bit "injector" process first spawns a 64 bit child, our "Deframe64.dat". They both perform the same main function at this stage - calling InstallHook() which is exported by the "guest" dll. I've messed with the output of Deframe64 so that it has the extension .dat; this is only an aesthetic step so that the user doesn't accidentally launch the useless and with no means of terminating 64 bit process. On 32 bit systems CreateProcess() will fail and I simply ignore everything involving it. Here's how the 32 bit injector terminates the 64 bit one:
if (bRes) ::PostThreadMessage(piDeframe64.dwThreadId, WM_QUIT, 0, 0);
http://elf.cs.pub.ro/so/wiki/
26-11-2011
11/15
// 3.1. do your stuff // 3.2. call thePatch(arguments) // 3.3. do other stuff // When done: // 4. restore the opcodes with thePatch.RemovePatch() // 5. the destructor or assigning a new function to thePatch will restore the protection status of the memory referred by target // Limitations: // won't work with variadic functions (blame C) namespace HotPatch { class Exception { }; class MemoryException : public Exception { }; class PatchException : public Exception { }; template <typename FType> class function_impl { public: function_impl() : _pFun(NULL), _pPatch(NULL), _detoured(false) { } ~function_impl() { _RestoreProtection(); } // res = function_impl<FType>::IsPatched() tells whether a call to the target will run the target or the hook. // Return value: // bool res - true if ApplyPatch() has been called without an accompanying RemovePatch(). bool IsPatched() { return _detoured; } // function_impl<FType>::operator=(pFun) directs the patch to a target and prepares the target memory for writes. // Parameters: // FType *pFun - the function to patch (see Apply()). // Throws: // MemoryException - when changing the protection of the page where pFun points is impossible. void operator=(FType *pFun) { _RestoreProtection(); _pFun = pFun; if (NULL == _pFun) return; BOOL res = VirtualProtect(_pFun, _64bit ? _PATCH_LENGTH64 : _PATCH_LENGTH32, PAGE_EXECUTE_READWRITE, &_oldProtection); if (!res) throw MemoryException(); } // function_impl<FType>::SetPatch(patch[, alwaysUse32Bit]) prepares internal state for the patch. // Parameters: // FType *patch - the patch to use instead of pFun (see operator=(FType *)). // bool alwaysUse32Bit - If true, a 32 bit jmp is always inserted. // Else, the best jmp is determined based on necessities (can still be 32 bit if it fits). // Throws: // PatchException - if patch is NULL. void SetPatch(FType *patch, bool alwaysUse32Bit = false) { if (NULL == patch) throw PatchException(); if (NULL != _pPatch) RemovePatch(); _pPatch = patch; _64bit = !alwaysUse32Bit; // they haven't expressed mandatory 32 bit only path; try to guess the best path
12/15
26-11-2011
if (_64bit) { LONGLONG jumpDistance = reinterpret_cast<LONGLONG>(_pPatch) reinterpret_cast<LONGLONG>(_pFun) _PATCH_LENGTH32; if (abs(jumpDistance) > 0x7FFFFFFF) // the jump is too long to fit a regular 32 bit relative jmp _64bit = true; else _64bit = false; } // save the old patch opcodes // plain old for is faster; DO NOT call any library functions! for (SIZE_T i = 0; i < (_64bit ? _PATCH_LENGTH64 : _PATCH_LENGTH32); ++i) *(_backup + i) = *(reinterpret_cast<PBYTE>(_pFun) + i); } // function_impl<FType>::ApplyPatch() makes pFun (see operator=(FType *)) jmp to patch (see SetPatch(FType *, bool)). // This is the actual hot patch mechanism at work. void ApplyPatch() { if (_64bit) { // movabs *reinterpret_cast<PBYTE>(_pFun) = _OP_MOVABS; // r11 *(reinterpret_cast<PBYTE>(_pFun) + 1) = _R11_WRITE; // _detourProc *reinterpret_cast<PLONGLONG>(reinterpret_cast<PBYTE>(_pFun) + 2) = reinterpret_cast<LONGLONG>(_pPatch); // jmp *reinterpret_cast<PWORD>(reinterpret_cast<PBYTE>(_pFun) + 10) = _OP_JMP64; // abs r11 *(reinterpret_cast<PBYTE>(_pFun) + 12) = _R11_JMP; } else { // jmp *reinterpret_cast<PBYTE>(_pFun) = _OP_JMP32; // distance left to _detourProc *reinterpret_cast<PDWORD>(reinterpret_cast<PBYTE>(_pFun) + 1) = static_cast<DWORD>( reinterpret_cast<PBYTE>(_pPatch) reinterpret_cast<PBYTE>(_pFun) static_cast<DWORD>(_PATCH_LENGTH32)); } _detoured = true; } // function_impl<FType>::RemovePatch() undoes what ApplyPatch() did. pFun (see operator=(FType *)) will be its old self again. void RemovePatch() { // plain old for is faster; DO NOT call any library functions! for (SIZE_T i = 0; i < (_64bit ? _PATCH_LENGTH64 : _PATCH_LENGTH32); ++i) *(reinterpret_cast<PBYTE>(_pFun) + i) = *(_backup + i); _detoured = false; } protected: FType *_pFun; FType *_pPatch; bool _64bit; bool _detoured; BYTE _backup[13]; DWORD _oldProtection; static static static static static const const const const const BYTE WORD BYTE BYTE BYTE _OP_JMP32 = 0xE9; _OP_JMP64 = 0xFF41; _OP_MOVABS = 0x49; _R11_WRITE = 0xBB; _R11_JMP = 0xE3; // jmp, detourProc - originalProc = 1 + 4 // movabs, R11, detourProc (64 bit), jmp (abs, 64), R11 = 1
static const SIZE_T _PATCH_LENGTH32 = 5; static const SIZE_T _PATCH_LENGTH64 = 13; + 1 + 8 + 2 + 1 template <typename T> static T abs(T val) { return val > 0 ? val : -val;
http://elf.cs.pub.ro/so/wiki/
26-11-2011
}
13/15
void _RestoreProtection() { if (NULL == _pFun) return; DWORD unusedOldProtection; BOOL res = VirtualProtect(_pFun, _64bit ? _PATCH_LENGTH64 : _PATCH_LENGTH32, _oldProtection, &unusedOldProtection); (void) res; // nothing to do } template <typename FType> class _NativeCallGuard { public: _NativeCallGuard(function_impl<FType> &fun) : _fun(fun) { _fun.RemovePatch(); } ~_NativeCallGuard() { _fun.ApplyPatch(); } private: function_impl<FType> &_fun; }; }; template <typename> class function; #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define HP_TARG0 HP_TARG1 , typename Arg1 HP_TARG2 HP_TARG1, typename Arg2 HP_TARG3 HP_TARG2, typename Arg3 HP_TARG4 HP_TARG3, typename Arg4 HP_TARG5 HP_TARG4, typename Arg5 HP_TARG6 HP_TARG5, typename Arg6 HP_TARG7 HP_TARG6, typename Arg7 HP_TARG8 HP_TARG7, typename Arg8 HP_TARG9 HP_TARG8, typename Arg9 HP_TARG10 HP_TARG9, typename Arg10 HP_FARG0 HP_FARG1 Arg1 HP_FARG2 HP_FARG1, Arg2 HP_FARG3 HP_FARG2, Arg3 HP_FARG4 HP_FARG3, Arg4 HP_FARG5 HP_FARG4, Arg5 HP_FARG6 HP_FARG5, Arg6 HP_FARG7 HP_FARG6, Arg7 HP_FARG8 HP_FARG7, Arg8 HP_FARG9 HP_FARG8, Arg9 HP_FARG10 HP_FARG9, Arg10 HP_ARG0 HP_ARG1 arg1 HP_ARG2 HP_ARG1, arg2 HP_ARG3 HP_ARG2, arg3 HP_ARG4 HP_ARG3, arg4 HP_ARG5 HP_ARG4, arg5 HP_ARG6 HP_ARG5, arg6 HP_ARG7 HP_ARG6, arg7 HP_ARG8 HP_ARG7, arg8 HP_ARG9 HP_ARG8, arg9 HP_ARG10 HP_ARG9, arg10 HP_ARG_DECL0 HP_ARG_DECL1 Arg1 arg1 HP_ARG_DECL2 HP_ARG_DECL1, Arg2 arg2 HP_ARG_DECL3 HP_ARG_DECL2, Arg3 arg3 HP_ARG_DECL4 HP_ARG_DECL3, Arg4 arg4 HP_ARG_DECL5 HP_ARG_DECL4, Arg5 arg5 HP_ARG_DECL6 HP_ARG_DECL5, Arg6 arg6 HP_ARG_DECL7 HP_ARG_DECL6, Arg7 arg7 HP_ARG_DECL8 HP_ARG_DECL7, Arg8 arg8 HP_ARG_DECL9 HP_ARG_DECL8, Arg9 arg9 HP_ARG_DECL10 HP_ARG_DECL9, Arg10 arg10
// template partial specialization for function<return_type([arg_types])> #define HP_RET_FUNCTION(n, callconv)\ template <typename _Ret HP_TARG##n>\ class function<_Ret callconv(HP_FARG##n)> : public function_impl<_Ret callconv(HP_FARG##n)>\ {\
14/15
26-11-2011
private:\ typedef _Ret callconv type(HP_FARG##n);\ \ public:\ ~function<type>()\ {\ _RestoreProtection();\ }\ \ using function_impl<type>::operator=;\ \ _Ret operator()(HP_ARG_DECL##n)\ {\ _NativeCallGuard<type> CallGuard(*this);\ return (*_pFun)(HP_ARG##n);\ }\ \ protected:\ using function_impl<type>::_pFun;\ } // declare the 11 templates handling functions in the form: // _Ret function() // _Ret function(Arg1) // _Ret function(Arg1, Arg2) // ... // _Ret function(Arg1, Arg2, ... Arg10) HP_RET_FUNCTION(0, __cdecl); HP_RET_FUNCTION(1, __cdecl); HP_RET_FUNCTION(2, __cdecl); HP_RET_FUNCTION(3, __cdecl); HP_RET_FUNCTION(4, __cdecl); HP_RET_FUNCTION(5, __cdecl); HP_RET_FUNCTION(6, __cdecl); HP_RET_FUNCTION(7, __cdecl); HP_RET_FUNCTION(8, __cdecl); HP_RET_FUNCTION(9, __cdecl); HP_RET_FUNCTION(10, __cdecl); #ifndef _M_X64 HP_RET_FUNCTION(0, __stdcall); HP_RET_FUNCTION(1, __stdcall); HP_RET_FUNCTION(2, __stdcall); HP_RET_FUNCTION(3, __stdcall); HP_RET_FUNCTION(4, __stdcall); HP_RET_FUNCTION(5, __stdcall); HP_RET_FUNCTION(6, __stdcall); HP_RET_FUNCTION(7, __stdcall); HP_RET_FUNCTION(8, __stdcall); HP_RET_FUNCTION(9, __stdcall); HP_RET_FUNCTION(10, __stdcall); #endif } // namespace HotPatch
What I'm doing above, is generating 11 partial tempate specializations for class HotPatch::function. The class is inspired by C++2011's std::function. It's a class that models functions with up to 10 (included) arguments - you can see the declarations through the HP_RET_FUNCTION macro calls in there. Then, another set of 11 declarations for x86-64 where only calling convention __cdecl exists. Do not worry abut understanding the above code; it's ugly and it sucks. Focus on the usage instead, it gets things done (for up to 10 arguments that is). To use this HotPatch mini-lib, write something like: HotPatchTest.cpp
#include <cstdio> #include "HotPatch.hpp" using namespace std; using namespace HotPatch; function<void()> fooPatch; void foo() { printf("In foo()\n"); } void bar()
http://elf.cs.pub.ro/so/wiki/
26-11-2011
{ printf("I'm hackin ur function\n"); if (fooPatch.IsPatched()) fooPatch(); } int main() { void (*pfoo)() = foo; void (*pbar)() = bar; pfoo(); // calls foo() pbar(); // calls bar() fooPatch = foo; fooPatch.SetPatch(bar); fooPatch.ApplyPatch(); pfoo(); // calls bar() pbar(); // calls bar() fooPatch.RemovePatch(); pfoo(); // calls foo() pbar(); // calls bar() return 0; }
15/15
From: http://elf.cs.pub.ro/so/wiki/ - Sisteme de Operare Permanent link: http://elf.cs.pub.ro/so/wiki/laboratoare/resurse/injections Last update: 2011/11/08 02:13