Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add better support for detouring at instruction addresses with DHooks #1969

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions extensions/dhooks/AMBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ for cxx in builder.targets:
os.path.join('DynamicHooks', 'conventions', 'x86MsCdecl.cpp'),
os.path.join('DynamicHooks', 'conventions', 'x86MsStdcall.cpp'),
os.path.join('DynamicHooks', 'conventions', 'x86MsFastcall.cpp'),
os.path.join('DynamicHooks', 'conventions', 'x86Instruction.cpp'),
]

if binary.compiler.target.platform == 'windows':
Expand Down
121 changes: 121 additions & 0 deletions extensions/dhooks/DynamicHooks/conventions/x86Instruction.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* =============================================================================
* DynamicHooks
* Copyright (C) 2015 Robin Gohmert. All rights reserved.
* Copyright (C) 2018-2021 AlliedModders LLC. All rights reserved.
* =============================================================================
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* asm.h/cpp from devmaster.net (thanks cybermind) edited by pRED* to handle gcc
* -fPIC thunks correctly
*
* Idea and trampoline code taken from DynDetours (thanks your-name-here).
*
* Adopted to provide similar features to SourceHook by AlliedModders LLC.
*/

// ============================================================================
// >> INCLUDES
// ============================================================================
#include "x86Instruction.h"
#include <string.h>


// ============================================================================
// >> x86Instruction
// ============================================================================
x86Instruction::x86Instruction(std::vector<DataTypeSized_t> &vecArgTypes, DataTypeSized_t returnType, int iAlignment) :
ICallingConvention(vecArgTypes, returnType, iAlignment)
{
}

x86Instruction::~x86Instruction()
{
}

std::vector<Register_t> x86Instruction::GetRegisters()
{
std::vector<Register_t> registers =
{
AL, CL, DL, BL, AH, CH, DH, BH,
EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI,
XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6, XMM7,
ST0
};

return registers;
}

int x86Instruction::GetPopSize()
{
return 0;
}

int x86Instruction::GetArgStackSize()
{
return 0;
}

void** x86Instruction::GetStackArgumentPtr(CRegisters* pRegisters)
{
return NULL;
}

int x86Instruction::GetArgRegisterSize()
{
int iArgRegisterSize = 0;

for (size_t i = 0; i < m_vecArgTypes.size(); i++)
{
if (m_vecArgTypes[i].custom_register != None)
iArgRegisterSize += m_vecArgTypes[i].size;
}

return iArgRegisterSize;
}

void* x86Instruction::GetArgumentPtr(unsigned int iIndex, CRegisters* pRegisters)
{
if (iIndex >= m_vecArgTypes.size())
return NULL;

// Check if this argument wasn't passed in a register.
if (m_vecArgTypes[iIndex].custom_register == None)
return NULL;

CRegister *pRegister = pRegisters->GetRegister(m_vecArgTypes[iIndex].custom_register);
if (!pRegister)
return NULL;

return pRegister->m_pAddress;
}

void x86Instruction::ArgumentPtrChanged(unsigned int iIndex, CRegisters* pRegisters, void* pArgumentPtr)
{
}

void* x86Instruction::GetReturnPtr(CRegisters* pRegisters)
{
return NULL;
}

void x86Instruction::ReturnPtrChanged(CRegisters* pRegisters, void* pReturnPtr)
{
}
68 changes: 68 additions & 0 deletions extensions/dhooks/DynamicHooks/conventions/x86Instruction.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* =============================================================================
* DynamicHooks
* Copyright (C) 2015 Robin Gohmert. All rights reserved.
* Copyright (C) 2018-2021 AlliedModders LLC. All rights reserved.
* =============================================================================
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* asm.h/cpp from devmaster.net (thanks cybermind) edited by pRED* to handle gcc
* -fPIC thunks correctly
*
* Idea and trampoline code taken from DynDetours (thanks your-name-here).
*
* Adopted to provide similar features to SourceHook by AlliedModders LLC.
*/

#ifndef _X86_INSTRUCTION_H
#define _X86_INSTRUCTION_H

// ============================================================================
// >> INCLUDES
// ============================================================================
#include "../convention.h"


// ============================================================================
// >> CLASSES
// ============================================================================
class x86Instruction: public ICallingConvention
{
public:
x86Instruction(std::vector<DataTypeSized_t> &vecArgTypes, DataTypeSized_t returnType, int iAlignment=4);
virtual ~x86Instruction();

virtual std::vector<Register_t> GetRegisters();
virtual int GetPopSize();
virtual int GetArgStackSize();
virtual void** GetStackArgumentPtr(CRegisters* pRegisters);
virtual int GetArgRegisterSize();

virtual void* GetArgumentPtr(unsigned int iIndex, CRegisters* pRegisters);
virtual void ArgumentPtrChanged(unsigned int iIndex, CRegisters* pRegisters, void* pArgumentPtr);

virtual void* GetReturnPtr(CRegisters* pRegisters);
virtual void ReturnPtrChanged(CRegisters* pRegisters, void* pReturnPtr);

private:
void* m_pReturnBuffer;
};

#endif // _X86_INSTRUCTION_H
30 changes: 21 additions & 9 deletions extensions/dhooks/DynamicHooks/hook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ using namespace sp;
// ============================================================================
// >> DEFINITIONS
// ============================================================================
#define JMP_SIZE 6
#define JMP_SIZE 5


// ============================================================================
Expand All @@ -57,6 +57,7 @@ CHook::CHook(void* pFunc, ICallingConvention* pConvention)
m_pFunc = pFunc;
m_pRegisters = new CRegisters(pConvention->GetRegisters());
m_pCallingConvention = pConvention;
m_pNewRetAddr = NULL;

if (!m_hookHandler.init())
return;
Expand Down Expand Up @@ -85,8 +86,8 @@ CHook::CHook(void* pFunc, ICallingConvention* pConvention)
// Save the trampoline
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful if you could skip instructions similar to the "supercede" mode for function calls.

Imagine a mid-function detour on something like mov eax, 0x1000 where you'd want to change the constant. If you add a detour, change eax, and return, the original instruction would still be executed and your detour had no effect.

I guess you can work around this by detouring after that instruction for now, but that could force you into jump target territory for no reason. I guess we can improve that in an additional PR. This feature is pretty advanced and you should be knowing what you're doing when going this way.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't even consider jump targets. Having a post routine would improve the situation, but it won't solve all cases as you mentioned yourself:

Detouring right before a jump-target might overwrite the following instruction causing undefined behavior when the function jumps into our inserted jmp instruction.

Copy link
Author

@chrb22 chrb22 Mar 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, don't really need a whole post routine for a single instruction, just a supercede implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that would just be conditionally replacing the m_pTrampoline jump with a m_pTrampoline + instruction size jump?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay that does seem to work. One problem is that you can't change params and supercede at the same time currently. Should I add MRES_ChangedSupercede = -3 to enum MRESReturn and implement that? Or just have it update params automatically when using MRES_Supercede?

Copy link
Author

@chrb22 chrb22 Apr 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about changing MRESReturn based on what you said about skipping a specified number of bytes

enum MRESReturn
{
	MRES_ChangedHandled = -2,       /**< Deprecated, use `MRES_Handled | MRES_Changed` instead */
	MRES_ChangedOverride = -1,      /**< Deprecated, use `MRES_Override | MRES_Changed` instead */
	MRES_Ignored = 0,               /**< plugin didn't take any action */
	MRES_Handled,                   /**< plugin did something, but real function should still be called */
	MRES_Override,                  /**< call real function, but use my return value */
	MRES_Supercede = 1 << 30,       /**< Skip hooked function/instruction. For functions you need to supply a return value.
	                                     For instructions you can optionally add a number to set the amount of instructions to skip (hooked instruction included in the count),
	                                     For example return `MRES_Supercede + 2` to skip 2 instructions. */
	MRES_Changed = 1 << 31,         /**< Add to return action to replace the params with your own values,
	                                     For example return `MRES_Handled | MRES_Changed` to change the function arguments via a pre-hook */
};

Does returning MRES_Handled actually do something different to returning MRES_Ignored?

m_pTrampoline = (void *) pCopiedBytes;

// Create the bridge function
m_pBridge = CreateBridge();
// Create the bridge function. A post hook can't be created if it isn't a function call and won't have a stack argument pointer.
m_pBridge = CreateBridge(pConvention->GetStackArgumentPtr(m_pRegisters) != NULL);

// Write a jump to the bridge
DoGatePatch((unsigned char *) pFunc, m_pBridge);
Expand All @@ -102,7 +103,8 @@ CHook::~CHook()

// Free the asm bridge and new return address
smutils->GetScriptingEngine()->FreePageMemory(m_pBridge);
smutils->GetScriptingEngine()->FreePageMemory(m_pNewRetAddr);
if (m_pNewRetAddr)
smutils->GetScriptingEngine()->FreePageMemory(m_pNewRetAddr);

delete m_pRegisters;
delete m_pCallingConvention;
Expand Down Expand Up @@ -229,13 +231,14 @@ void __cdecl CHook::SetReturnAddress(void* pRetAddr, void* pESP)
i->value.push_back(pRetAddr);
}

void* CHook::CreateBridge()
void* CHook::CreateBridge(bool isFunctionHook)
{
sp::MacroAssembler masm;
Label label_supercede;

// Write a redirect to the post-hook code
Write_ModifyReturnAddress(masm);
if (isFunctionHook)
Write_ModifyReturnAddress(masm);

// Call the pre-hook handler and jump to label_supercede if ReturnAction_Supercede was returned
Write_CallHandler(masm, HOOKTYPE_PRE);
Expand All @@ -252,9 +255,18 @@ void* CHook::CreateBridge()
// This code will be executed if a pre-hook returns ReturnAction_Supercede
masm.bind(&label_supercede);

// Finally, return to the caller
// This will still call post hooks, but will skip the original function.
masm.ret(m_pCallingConvention->GetPopSize());
if (isFunctionHook)
{
// Finally, return to the caller
// This will still call post hooks, but will skip the original function.
masm.ret(m_pCallingConvention->GetPopSize());
}
else
{
// Jump to the instruction right after the hooked instruction inside the trampoline
void* target = (char*)m_pTrampoline + instruction_length(m_pFunc);
masm.jmp(ExternalAddress(target));
}

void *base = smutils->GetScriptingEngine()->AllocatePageMemory(masm.length());
masm.emitToExecutableMemory(base);
Expand Down
2 changes: 1 addition & 1 deletion extensions/dhooks/DynamicHooks/hook.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class CHook
}

private:
void* CreateBridge();
void* CreateBridge(bool createPostHook);

void Write_ModifyReturnAddress(sp::MacroAssembler& masm);
void Write_CallHandler(sp::MacroAssembler& masm, HookType_t type);
Expand Down
8 changes: 8 additions & 0 deletions extensions/dhooks/dynhooks_sourcepawn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,25 @@
#include "conventions/x86MsThiscall.h"
#include "conventions/x86MsStdcall.h"
#include "conventions/x86MsFastcall.h"
#include "conventions/x86Instruction.h"
typedef x86MsCdecl x86DetourCdecl;
typedef x86MsThiscall x86DetourThisCall;
typedef x86MsStdcall x86DetourStdCall;
typedef x86MsFastcall x86DetourFastCall;
typedef x86Instruction x86DetourInstruction;
#elif defined KE_LINUX
#include "conventions/x86GccCdecl.h"
#include "conventions/x86GccThiscall.h"
#include "conventions/x86MsStdcall.h"
#include "conventions/x86MsFastcall.h"
#include <conventions/x86Instruction.h>
typedef x86GccCdecl x86DetourCdecl;
typedef x86GccThiscall x86DetourThisCall;
// Uhm, stdcall on linux?
typedef x86MsStdcall x86DetourStdCall;
// Uhumm, fastcall on linux?
typedef x86MsFastcall x86DetourFastCall;
typedef x86Instruction x86DetourInstruction;
#else
#error "Unsupported platform."
#endif
Expand Down Expand Up @@ -241,6 +245,10 @@ ICallingConvention *ConstructCallingConvention(HookSetup *setup)
break;
case CallConv_FASTCALL:
pCallConv = new x86DetourFastCall(vecArgTypes, returnType);
break;
case CallConv_INSTRUCTION:
pCallConv = new x86DetourInstruction(vecArgTypes, returnType);

break;
default:
smutils->LogError(myself, "Unknown calling convention %d.", setup->callConv);
Expand Down
15 changes: 15 additions & 0 deletions extensions/dhooks/natives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ cell_t Native_CreateHook(IPluginContext *pContext, const cell_t *params)
//native Handle:DHookCreateDetour(Address:funcaddr, CallingConvention:callConv, ReturnType:returntype, ThisPointerType:thistype);
cell_t Native_CreateDetour(IPluginContext *pContext, const cell_t *params)
{
if ((CallingConvention)params[2] == CallConv_INSTRUCTION)
{
if ((ReturnType)params[3] != ReturnType_Void)
return pContext->ThrowNativeError("Return type must be void for an instruction hook.");

if ((ThisPointerType)params[4] != ThisPointer_Ignore)
return pContext->ThrowNativeError("This pointer must be ignored for an instruction hook.");
}

HookSetup *setup = new HookSetup((ReturnType)params[3], PASSFLAG_BYVAL, (CallingConvention)params[2], (ThisPointerType)params[4], (void *)params[1]);

Handle_t hndl = handlesys->CreateHandle(g_HookSetupHandle, setup, pContext->GetIdentity(), myself->GetIdentity(), NULL);
Expand Down Expand Up @@ -315,6 +324,9 @@ cell_t Native_AddParam(IPluginContext *pContext, const cell_t *params)
info.custom_register = None;
}

if (setup->callConv == CallConv_INSTRUCTION && info.custom_register == None)
return pContext->ThrowNativeError("Must specify registers for parameters in an instruction hook.");

if(params[0] >= 3 && params[3] != -1)
{
info.size = params[3];
Expand Down Expand Up @@ -359,6 +371,9 @@ cell_t Native_EnableDetour(IPluginContext *pContext, const cell_t *params)
bool post = params[2] != 0;
HookType_t hookType = post ? HOOKTYPE_POST : HOOKTYPE_PRE;

if (setup->callConv == CallConv_INSTRUCTION && hookType == HOOKTYPE_POST)
return pContext->ThrowNativeError("Can't create a post hook for instruction hooks.");

// Check if we already detoured that function.
CHookManager *pDetourManager = GetHookManager();
CHook* pDetour = pDetourManager->FindHook(setup->funcAddr);
Expand Down
Loading