Files
2021-01-17 22:23:01 -08:00

343 lines
13 KiB
C

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/user.h>
#include "libdis.h"
#include "hook.h"
// Routines to intercept or redirect routines.
// Author: Tavis Ormandy
// This was chosen arbitrarily, the maximum amount of code we will search to
// find a call when looking for callsites, feel free to adjust as required.
#define MAX_FUNCTION_LENGTH 2048
// A redirect is usually 9 bytes (5 bytes of call, 4 bytes of encoded size),
// but we round it up to the next instruction boundary. Because of this, the
// worst possible case would be a 8 byte instruction, followed by 16 byte
// instruction (where 16 is the longest possible instruction intel allows).
#define MAX_REDIRECT_LENGTH 24
static void __attribute__((constructor)) init(void)
{
// Initialize libdisasm.
x86_init(opt_none, NULL, NULL);
}
// Intercept calls to this function and execute redirect first. Depending on
// flags, you can either replace this function, or simply be inserted into the
// call chain.
// function The address of the function you want intercepted.
// redirect Your callback function. The prototype should be the same as
// function, except an additional first parameter which you can
// ignore (it's the return address for the caller).
// flags Options, see header file for flags available. Use HOOK_DEFAULT
// if you don't need any.
//
// Remember to add an additional parameter to your redirect, e.g. if you were
// expecting tcp_input(struct mbuf *m, int len), your redirect should be:
//
// my_tcp_input(intptr_t retaddr, struct mbuf *m, int len);
//
// *UNLESS* You are using the flag HOOK_REPLACE_FUNCTION, in which case the
// prototype is the same, as you literally become the function instead of
// intercepting it.
bool insert_function_redirect(void *function, void *redirect, uint32_t flags)
{
size_t redirectsize = 0;
unsigned insncount = 0;
struct branch *fixup;
struct branch *callsite;
struct branch *restore;
struct encodedsize *savedoffset;
// Keep disassembling until I have enough bytes of code to store my
// redirect, five bytes for the redirect call, and four bytes to record the
// length to restore when we're unloaded.
//
// XXX: If there is a branch target or return within the first 9 bytes, I'm
// screwed. Seems unlikely though, so I'm not worrying about it right
// now. I could at least check for rets?
//
for (redirectsize = 0; redirectsize < sizeof(struct branch) + sizeof(struct encodedsize); insncount++) {
x86_insn_t insn = {0};
ssize_t insnlength = 0;
// Test if libdisasm understood the instruction
if ((insnlength = x86_disasm(function, MAX_REDIRECT_LENGTH, (uintptr_t)(function), redirectsize, &insn))) {
// Valid, increment size.
redirectsize += insnlength;
// Check for branches just to be safe, as these instructions are
// relative and cannot be relocated safely (there are others of
// course, but these are the most likely).
if (insn.group == insn_controlflow && flags != HOOK_REPLACE_FUNCTION) {
printf("error: refusing to redirect function %p due to early controlflow manipulation (+%u)\n",
function,
redirectsize);
// Clean up.
x86_oplist_free(&insn);
return false;
}
// Clean up.
x86_oplist_free(&insn);
// Next instuction.
continue;
}
// Invalid instruction, abort.
printf("error: %s encountered an invalid instruction @%p+%u, so redirection was aborted\n",
__func__,
function,
redirectsize);
return false;
}
// We need to create a fixup, a small chunk of code that repairs the damage
// we did redirecting the function. This basically handles calling the
// redirect, then fixes the damage and restores execution. So it's going to be
// redirectsize + 2 * sizeof(struct branch) bytes, which looks like this:
//
// call your_routine ; 5 bytes
// <code clobbered to get here> ; redirectsize bytes
// jmp original_routine+redirectsize ; 5 bytes
//
// Your routine will get an extra first argument which you should
// ignore, e.g.
//
// void your_routine(uintptr_t retaddr, int expected_arg1, void *expected_arg2, etc);
//
// If you replace the function instead of redirect it, you don't get the extra
// parameter, because we literally just jmp to your routine instead of call
// it. The call operand is a relative, displaced address, hence the
// calculation.
//
fixup = calloc(redirectsize + sizeof(struct branch) * 2, 1);
fixup->opcode = flags & HOOK_REPLACE_FUNCTION
? X86_OPCODE_JMP_NEAR
: X86_OPCODE_CALL_NEAR;
fixup->operand.i = (uintptr_t)(redirect)
- (uintptr_t)(fixup)
- (uintptr_t)(sizeof(struct branch));
// Fix permissions on the redirect.
if (mprotect((void *)((uintptr_t) fixup & PAGE_MASK), PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
printf("mprotect() failed on stub => %p (%m), try `sudo setenforce 0`\n", fixup);
return false;
}
// Copy over the code we are going to clobber by installing the redirect.
memcpy(&fixup->data, function, redirectsize);
// And install a branch to restore execution to the rest of the original routine.
restore = (void *)(fixup->data + redirectsize);
restore->opcode = X86_OPCODE_JMP_NEAR;
restore->operand.i = (uintptr_t)(function)
+ (uintptr_t)(redirectsize)
- (uintptr_t)(restore)
- (uintptr_t)(sizeof(struct branch));
// Now I need to install the redirect, I also clobber any left over bytes
// with x86 nops, so as not to disrupt disassemblers while debugging.
callsite = function;
// In general this is expected to be called on functions.
if (callsite->opcode != X86_OPCODE_PUSH_EBP) {
#ifndef NDEBUG
printf("warning: requested hook location %p does not look like a function, begins with opcode %#02x.\n",
callsite,
callsite->opcode);
#endif
}
callsite->opcode = X86_OPCODE_JMP_NEAR;
callsite->operand.i = (uintptr_t)(fixup)
- (uintptr_t)(callsite)
- (uintptr_t)(sizeof(struct branch));
// I need to remember how much data I clobbered so that I can restore it
// when my module is unloaded. I do this by encoding it as an instruction, e.g.
//
// mov eax, imm16
//
// This is so as not to disrupt disassembly.
savedoffset = (void *)(callsite->data);
savedoffset->prefix = X86_PREFIX_DATA16;
savedoffset->opcode = X86_OPCODE_MOV_EAX_IMM;
savedoffset->operand = redirectsize;
// Clean up the left over slack bytes (not acutally needed, as we're careful to
// restore execution to the next valid instructions, but intended to make
// sure we dont desync disassembly when debugging problems in kgdb).
memset(callsite->data + sizeof(struct encodedsize),
X86_OPCODE_NOP,
redirectsize - sizeof(struct branch) - sizeof(struct encodedsize));
//printf("info: successfully installed %lu byte (%u instructions) redirect from %p to %p, via fixup@%p\n",
// redirectsize,
// insncount,
// function,
// redirect,
// fixup);
return true;
}
// This routine will simply remove a previously inserted redirect. It's careful
// to verify there really is a redirect present, but you should probably be
// careful.
//
// function The location of the redirected function to restore.
//
bool remove_function_redirect(void *function)
{
struct branch *callsite;
struct encodedsize *savedsize;
void *fixup;
// The process for removal is:
//
// * Read the branch instuction and the encoded size from the original location.
// * From this, calculate the fixup address.
// * Restore the clobbered data from the fixup to the function using the
// size I recorded in the original function.
// * FREE() the fixup.
//
// And that's it, so let's grab the branch instruction.
callsite = function;
fixup = (void *)((uintptr_t)(callsite->operand.i)
+ (uintptr_t)(callsite)
+ (uintptr_t)(sizeof(struct branch)));
savedsize = (void *)(callsite->data);
// Let's verify this looks sane.
if (callsite->opcode != X86_OPCODE_JMP_NEAR) {
printf("error: tried to remove function hook from %p, but it didnt contain a redirect (%02x)\n",
function,
callsite->opcode);
return false;
}
// Check the encoded size looks sane.
if (savedsize->opcode != X86_OPCODE_MOV_EAX_IMM
|| savedsize->prefix != X86_PREFIX_DATA16
|| savedsize->operand > MAX_REDIRECT_LENGTH) {
printf("error: tried to remove function hook from %p, but encoded size did not validate { %02x %02x %04x }\n",
function,
savedsize->prefix,
savedsize->opcode,
savedsize->operand);
return false;
}
// Restore clobbered code. Remember the fixup contains two branches, the
// call at the start and the jmp at the end, we only want to restore the
// clobbered data in the middle.
memcpy(function, fixup + sizeof(struct branch), savedsize->operand);
// Check it looks sane.
if (callsite->opcode != X86_OPCODE_PUSH_EBP) {
printf("warning: restored location %p does not look like a function %02x.\n",
function,
callsite->opcode);
}
printf("info: successfully removed redirect from %p, via fixup@%p\n",
function,
fixup);
// Release memory.
free(fixup);
return true;
}
// Replace a call within an arbitrary function. Call as many times as you need
// on the same function. To reverse the operation, simply call again but switch
// the target and redirect parameters.
//
// function The function that contains the call you want to intercept.
// target The function that is called by @function that you want to intercept.
// redirect What to call instead.
//
// For example, if tcp_input contains a call to inet_cksum, and you want to
// intercept that call, but not every call to inet_cksum, you can do this:
//
// redirect_call_within_function(tcp_input, inet_cksum, my_cksum_replacement);
//
bool redirect_call_within_function(void *function, void *target, void *redirect)
{
size_t offset = 0;
struct branch *callsite = NULL;
while (true) {
x86_insn_t insn;
ssize_t insnlength;
// Test if libdisasm understood the instruction
if ((insnlength = x86_disasm(function, MAX_FUNCTION_LENGTH, (uintptr_t)(function), offset, &insn))) {
// Examine the instuction found to see if it matches the call we
// want to replace.
if (insn.type == insn_call) {
if (x86_get_rel_offset(&insn) == (uintptr_t)(target)
- (uintptr_t)(function + offset)
- (uintptr_t)(insnlength)) {
// Success, this is the location the caller wants us to patch.
callsite = (struct branch *)(function + offset);
// Let's move on to patching.
printf("info: found a call at %p, the target is %#x\n", callsite, x86_get_rel_offset(&insn));
// Clean up, then exit disassembly.
x86_oplist_free(&insn);
break;
}
}
// Valid, but not interesting. Increment size.
offset += insnlength;
// Clean up.
x86_oplist_free(&insn);
// Next instuction.
continue;
}
// Invalid instruction, abort.
printf("error: %s encountered an invalid instruction or end of stream @%p+%u, so redirection was aborted\n",
__func__,
function,
offset);
return false;
}
// callsite is the call instruction we're supposed to patch, so insert the
// new target.
callsite->operand.i = (uintptr_t)(redirect)
- (uintptr_t)(callsite)
- (uintptr_t)(sizeof(struct branch));
printf("info: successfully redirected call to %p at %p+%x with a call to %p\n",
target,
function,
offset,
redirect);
// Complete.
return true;
}