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
117 changes: 117 additions & 0 deletions extensions/dhooks/DynamicHooks/conventions/x86Instruction.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* =============================================================================
* 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;

peace-maker marked this conversation as resolved.
Show resolved Hide resolved
// Save all the custom calling convention registers as well.
for (size_t i = 0; i < m_vecArgTypes.size(); i++)
{
if (m_vecArgTypes[i].custom_register == None)
continue;

// TODO: Make sure the list is unique? Set?
registers.push_back(m_vecArgTypes[i].custom_register);
}

return registers;
}

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

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

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

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

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
16 changes: 10 additions & 6 deletions extensions/dhooks/DynamicHooks/hook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 createPostHook)
{
sp::MacroAssembler masm;
Label label_supercede;

// Write a redirect to the post-hook code
Write_ModifyReturnAddress(masm);
if (createPostHook)
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 @@ -244,7 +247,8 @@ void* CHook::CreateBridge()
// Restore the previously saved registers, so any changes will be applied
Write_RestoreRegisters(masm, HOOKTYPE_PRE);

masm.j(equal, &label_supercede);
if (createPostHook)
masm.j(equal, &label_supercede);

// Jump to the trampoline
masm.jmp(ExternalAddress(m_pTrampoline));
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
16 changes: 16 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 Expand Up @@ -424,6 +432,14 @@ ReturnAction_t HandleDetour(HookType_t hookType, CHook* pDetour)
}
}

// It doesn't make sense to supercede instruction detours when they're detouring individual instructions
if (pWrapper->callConv == CallConv_INSTRUCTION && result == MRES_Supercede)
{
tempRet = ReturnAction_Ignored;
pCallback->GetParentRuntime()->GetDefaultContext()->BlamePluginError(pCallback, "Tried to supercede an instruction detour which is not possible");
break;
}

// Store if the plugin wants the original function to be called.
if (result == MRES_Supercede)
tempRet = ReturnAction_Supercede;
Expand Down
30 changes: 30 additions & 0 deletions extensions/dhooks/natives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ IPluginFunction *GetCallback(IPluginContext *pContext, HookSetup * setup, const
//native Handle:DHookCreate(offset, HookType:hooktype, ReturnType:returntype, ThisPointerType:thistype, DHookCallback:callback = INVALID_FUNCTION); // Callback is now optional here.
cell_t Native_CreateHook(IPluginContext *pContext, const cell_t *params)
{
if ((CallingConvention)params[2] == CallConv_INSTRUCTION)
peace-maker marked this conversation as resolved.
Show resolved Hide resolved
return pContext->ThrowNativeError("Can't create an instruction hook from a vtable offset.");

IPluginFunction *callback = nullptr;
// The methodmap constructor doesn't have the callback parameter anymore.
if (params[0] >= 5)
Expand All @@ -122,6 +125,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 @@ -185,6 +197,18 @@ cell_t Native_DHookCreateFromConf(IPluginContext *pContext, const cell_t *params
}
}

if (sig->callConv == CallConv_INSTRUCTION)
peace-maker marked this conversation as resolved.
Show resolved Hide resolved
{
if (sig->retType != ReturnType_Void)
return pContext->ThrowNativeError("Return type must be void for an instruction hook.");

for (ArgumentInfo &arg : sig->args)
{
if (arg.info.custom_register != None)
return pContext->ThrowNativeError("Must specify registers for parameters in an instruction hook.");
}
}

setup = new HookSetup(sig->retType, PASSFLAG_BYVAL, sig->callConv, sig->thisType, addr);
}

Expand Down Expand Up @@ -315,6 +339,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 +386,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
2 changes: 2 additions & 0 deletions extensions/dhooks/signatures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ SMCResult SignatureGameConfig::ReadSMC_KeyValue(const SMCStates *states, const c
callConv = CallConv_STDCALL;
else if (!strcmp(value, "fastcall"))
callConv = CallConv_FASTCALL;
else if (!strcmp(value, "instruction"))
callConv = CallConv_INSTRUCTION;
else
{
smutils->LogError(myself, "Invalid calling convention \"%s\": line: %i col: %i", value, states->line, states->col);
Expand Down
1 change: 1 addition & 0 deletions extensions/dhooks/vhook.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ enum CallingConvention
CallConv_THISCALL,
CallConv_STDCALL,
CallConv_FASTCALL,
CallConv_INSTRUCTION,
};

enum MRESReturn
Expand Down
Loading