// Copyright 2015-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "HardHook.h"
#include "ods.h"

void *HardHook::pCode         = nullptr;
unsigned int HardHook::uiCode = 0;

const int HardHook::CODEREPLACESIZE = 6;
const int HardHook::CODEPROTECTSIZE = 16;

/**
 * @brief Constructs a new hook without actually injecting.
 */
HardHook::HardHook() : bTrampoline(false), call(0), baseptr(nullptr) {
	for (int i = 0; i < CODEREPLACESIZE; ++i) {
		orig[i] = replace[i] = 0;
	}

	//	assert(CODEREPLACESIZE == sizeof(orig) / sizeof(orig[0]));
	//	assert(CODEREPLACESIZE == sizeof(replace) / sizeof(replace[0]));
}

/**
 * @brief Constructs a new hook by injecting given replacement function into func.
 * @see HardHook::setup
 * @param func Funktion to inject replacement into.
 * @param replacement Function to inject into func.
 */
HardHook::HardHook(voidFunc func, voidFunc replacement) : bTrampoline(false), call(0), baseptr(nullptr) {
	for (int i = 0; i < CODEREPLACESIZE; ++i)
		orig[i] = replace[i] = 0;
	setup(func, replacement);
}

/**
 * @return Number of extra bytes.
 */
static unsigned int modrmbytes(unsigned char a, unsigned char b) {
	unsigned char lower = (a & 0x0f);
	if (a >= 0xc0) {
		return 0;
	} else if (a >= 0x80) {
		if ((lower == 4) || (lower == 12))
			return 5;
		else
			return 4;
	} else if (a >= 0x40) {
		if ((lower == 4) || (lower == 12))
			return 2;
		else
			return 1;

	} else {
		if ((lower == 4) || (lower == 12)) {
			if ((b & 0x07) == 0x05)
				return 5;
			else
				return 1;
		} else if ((lower == 5) || (lower == 13))
			return 4;
		return 0;
	}
}

/**
 * @brief Tries to construct a trampoline from original code.
 *
 * A trampoline is the replacement code that features the original code plus
 * a jump back to the original instructions that follow.
 * It is called to execute the original behavior. As it is a replacement for
 * the original, the original can then be overwritten.
 * The size of the trampoline is at least CODEREPLACESIZE. Thus, CODEREPLACESIZE
 * bytes of the original code can afterwards be overwritten (and the trampoline
 * called after those instructions for the original logic).
 * CODEREPLACESIZE has to be smaller than CODEPROTECTSIZE.
 *
 * As commands must not be destroyed they have to be disassembled to get their length.
 * All encountered commands will be part of the trampoline and stored in pCode (shared
 * for all trampolines).
 *
 * If code is encountered that can not be moved into the trampoline (conditionals etc.)
 * construction fails and nullptr is returned. If enough commands can be saved the
 * trampoline is finalized by appending a jump back to the original code. The return value
 * in this case will be the address of the newly constructed trampoline.
 *
 * pCode + offset to trampoline:
 *     [SAVED CODE FROM ORIGINAL which is >= CODEREPLACESIZE bytes][JUMP BACK TO ORIGINAL CODE]
 *
 * @param porig Original code
 * @return Pointer to trampoline on success. nullptr if trampoline construction failed.
 */
void *HardHook::cloneCode(void **porig) {
	if (!pCode || uiCode > 4000) {
		pCode  = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
		uiCode = 0;
	}
	// If we have no memory to clone to, return.
	if (!pCode) {
		return nullptr;
	}

	unsigned char *o = (unsigned char *) *porig;

	DWORD origProtect;
	if (!VirtualProtect(o, CODEPROTECTSIZE, PAGE_EXECUTE_READ, &origProtect)) {
		fods("HardHook: CloneCode failed; failed to make original code read and executable");
		return nullptr;
	}

	// Follow relative jumps to next instruction. On execution it doesn't make
	// a difference if we actually perform all the jumps or directly jump to the
	// end of the chain. Hence these jumps need not be part of the trampoline.
	while (*o == 0xe9) { // JMP
		unsigned char *tmp = o;
		int *iptr          = reinterpret_cast< int * >(o + 1);
		o += *iptr + 5;

		fods("HardHook: CloneCode: Skipping jump from %p to %p", *porig, o);
		*porig = o;

		// Assume jump took us out of our read enabled zone, get rights for the new one
		DWORD tempProtect;
		VirtualProtect(tmp, CODEPROTECTSIZE, origProtect, &tempProtect);
		if (!VirtualProtect(o, CODEPROTECTSIZE, PAGE_EXECUTE_READ, &origProtect)) {
			fods("HardHook: CloneCode failed; failed to make jump target code read and executable");
			return nullptr;
		}
	}

	unsigned char *n = (unsigned char *) pCode;
	n += uiCode;
	unsigned int idx = 0;

	do {
		unsigned char opcode = o[idx];
		unsigned char a      = o[idx + 1];
		unsigned char b      = o[idx + 2];
		unsigned int extra   = 0;

		switch (opcode) {
			case 0x50: // PUSH
			case 0x51:
			case 0x52:
			case 0x53:
			case 0x54:
			case 0x55:
			case 0x56:
			case 0x57:
			case 0x58: // POP
			case 0x59:
			case 0x5a:
			case 0x5b:
			case 0x5c:
			case 0x5d:
			case 0x5e:
			case 0x5f:
				break;
			case 0x6a: // PUSH immediate
				extra = 1;
				break;
			case 0x68: // PUSH immediate
				extra = 4;
				break;
			case 0x81: // CMP immediate
				extra = modrmbytes(a, b) + 5;
				break;
			case 0x83: // CMP
				extra = modrmbytes(a, b) + 2;
				break;
			case 0x8b: // MOV
				extra = modrmbytes(a, b) + 1;
				break;
			default: {
				int rmop = ((a >> 3) & 7);
				if (opcode == 0xff && rmop == 6) { // PUSH memory
					extra = modrmbytes(a, b) + 1;
					break;
				}

				fods("HardHook: CloneCode failed; Unknown opcode %02x at %d: %02x %02x %02x %02x %02x %02x %02x %02x "
					 "%02x %02x %02x %02x",
					 opcode, idx, o[0], o[1], o[2], o[3], o[4], o[5], o[6], o[7], o[8], o[9], o[10], o[11]);
				DWORD tempProtect;
				VirtualProtect(o, CODEPROTECTSIZE, origProtect, &tempProtect);
				return nullptr;
				break;
			}
		}

		n[idx] = opcode;
		++idx;

		for (unsigned int i = 0; i < extra; ++i)
			n[idx + i] = o[idx + i];
		idx += extra;

	} while (idx < CODEREPLACESIZE);

	DWORD tempProtect;
	VirtualProtect(o, CODEPROTECTSIZE, origProtect, &tempProtect);

	// Add a relative jmp back to the original code, to after the copied code
	n[idx++]              = 0xe9;
	int *iptr             = reinterpret_cast< int * >(&n[idx]);
	const int JMP_OP_SIZE = 5;
	int offs              = o - n - JMP_OP_SIZE;
	*iptr                 = offs;
	idx += 4;

	uiCode += idx;

	FlushInstructionCache(GetCurrentProcess(), n, idx);

	fods("HardHook: trampoline creation successful at %p", n);

	return n;
}

/**
 * @brief Makes sure the given replacement function is run once func is called.
 *
 * Tries to construct a trampoline for the given function (@see HardHook::cloneCode)
 * and then injects replacement function calling code into the first 6 bytes of the
 * original function (@see HardHook::inject).
 *
 * @param func Pointer to function to redirect.
 * @param replacement Pointer to code to redirect to.
 */
void HardHook::setup(voidFunc func, voidFunc replacement) {
	if (baseptr)
		return;

	fods("HardHook: Setup: Asked to replace %p with %p", func, replacement);

	unsigned char *fptr = reinterpret_cast< unsigned char * >(func);
	unsigned char *nptr = reinterpret_cast< unsigned char * >(replacement);

	call = (voidFunc) cloneCode((void **) &fptr);

	if (call) {
		bTrampoline = true;
	} else {
		// Could not create a trampoline. Use alternative method instead.
		// This alternative method is dependant on the replacement code
		// restoring before calling the original. Otherwise we get a jump recursion
		bTrampoline = false;
		call        = func;
	}

	DWORD origProtect;
	if (VirtualProtect(fptr, CODEPROTECTSIZE, PAGE_EXECUTE_READ, &origProtect)) {
		replace[0]           = 0x68; // PUSH immediate        1 Byte
		unsigned char **iptr = reinterpret_cast< unsigned char ** >(&replace[1]);
		*iptr                = nptr; // (imm. value = nptr)   4 Byte
		replace[5]           = 0xc3; // RETN                  1 Byte

		// Save original 6 bytes at start of original function
		for (int i = 0; i < CODEREPLACESIZE; ++i)
			orig[i] = fptr[i];

		baseptr = fptr;

		inject(true);

		DWORD tempProtect;
		VirtualProtect(fptr, CODEPROTECTSIZE, origProtect, &tempProtect);
	} else {
		fods("HardHook: setup failed; failed to make original code read and executable");
	}
}

void HardHook::setupInterface(IUnknown *unkn, LONG funcoffset, voidFunc replacement) {
	fods("HardHook: setupInterface: Replacing %p function #%ld", unkn, funcoffset);
	void **ptr = reinterpret_cast< void ** >(unkn);
	ptr        = reinterpret_cast< void ** >(ptr[0]);
	setup(reinterpret_cast< voidFunc >(ptr[funcoffset]), replacement);
}

void HardHook::reset() {
	baseptr     = 0;
	bTrampoline = false;
	call        = nullptr;
	for (int i = 0; i < CODEREPLACESIZE; ++i) {
		orig[i] = replace[i] = 0;
	}
}

/**
 * @brief Injects redirection code into the target function.
 *
 * Replaces the first 6 Bytes of the function indicated by baseptr
 * with the replacement code previously generated (usually a jump
 * to mumble code). If a trampoline is available this injection is not needed
 * as control flow was already permanently redirected by HardHook::setup .
 *
 * @param force Perform injection even when trampoline is available.
 */
void HardHook::inject(bool force) {
	if (!baseptr)
		return;
	if (!force && bTrampoline)
		return;

	DWORD origProtect;
	if (VirtualProtect(baseptr, CODEREPLACESIZE, PAGE_EXECUTE_READWRITE, &origProtect)) {
		for (int i = 0; i < CODEREPLACESIZE; ++i) {
			baseptr[i] = replace[i]; // Replace with jump to new code
		}

		DWORD tempProtect;
		VirtualProtect(baseptr, CODEREPLACESIZE, origProtect, &tempProtect);

		FlushInstructionCache(GetCurrentProcess(), baseptr, CODEREPLACESIZE);
	}

	// Verify that the injection was successful
	for (int i = 0; i < CODEREPLACESIZE; ++i) {
		if (baseptr[i] != replace[i]) {
			fods("HardHook: Injection failure noticed at byte %d", i);
		}
	}
}

/**
 * @brief Restores the original code in a target function.
 *
 * Restores the first 6 Bytes of the function indicated by baseptr
 * from previously stored original code in orig. If a trampoline is available this
 * restoration is not needed as trampoline will correctly restore control
 * flow.
 *
 * @param force If true injection will be reverted even when trampoline is available.
 */
void HardHook::restore(bool force) {
	if (!baseptr)
		return;
	if (!force && bTrampoline)
		return;

	DWORD origProtect;
	if (VirtualProtect(baseptr, CODEREPLACESIZE, PAGE_EXECUTE_READWRITE, &origProtect)) {
		for (int i = 0; i < CODEREPLACESIZE; ++i)
			baseptr[i] = orig[i];
		DWORD tempProtect;
		VirtualProtect(baseptr, CODEREPLACESIZE, origProtect, &tempProtect);

		FlushInstructionCache(GetCurrentProcess(), baseptr, CODEREPLACESIZE);
	}
}

void HardHook::print() {
	fods("HardHook: code replacement: %02x %02x %02x %02x %02x => %02x %02x %02x %02x %02x (currently effective: %02x "
		 "%02x %02x %02x %02x)",
		 orig[0], orig[1], orig[2], orig[3], orig[4], replace[0], replace[1], replace[2], replace[3], replace[4],
		 baseptr[0], baseptr[1], baseptr[2], baseptr[3], baseptr[4]);
}

/**
 * @brief Checks whether injected code is in good shape and injects if not yet injected.
 *
 * If injected code is not found injection is attempted unless 3rd party overwrote
 * original code at injection location.
 */
void HardHook::check() {
	if (memcmp(baseptr, replace, CODEREPLACESIZE) != 0) {
		// The instructions do not match our replacement instructions
		// If they match the original code, inject our hook.
		if (memcmp(baseptr, orig, CODEREPLACESIZE) == 0) {
			fods("HardHook: Reinjecting hook into function %p", baseptr);
			inject(true);
		} else {
			fods("HardHook: Function %p replaced by third party. Lost injected hook.");
		}
	}
}
