Skip to content

Instantly share code, notes, and snippets.

@tgfrerer
Last active December 8, 2023 15:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tgfrerer/ddede651d181ca28ad6999c5104b268d to your computer and use it in GitHub Desktop.
Save tgfrerer/ddede651d181ca28ad6999c5104b268d to your computer and use it in GitHub Desktop.
Proof of concept for "Callbacks and Hot-Reloading Reloaded: Bring your own PLT" -- https://poniesandlight.co.uk/reflect/bring_your_own_plt/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#ifdef _WIN32
# include <windows.h>
# include <cassert>
#else
# include <unistd.h>
# include <sys/mman.h>
#endif
static void say( char const* utterance ) {
puts( "I say: " );
puts( utterance );
}
static void shout( char const* utterance ) {
puts( "I shout: " );
puts( utterance );
}
int main( void ) {
typedef void( say_fun_t )( char const* );
#ifdef _WIN32
SYSTEM_INFO sys_info;
GetSystemInfo( &sys_info );
size_t page_size = sys_info.dwPageSize;
#else
size_t page_size = sysconf( _SC_PAGESIZE );
#endif
// Let's get two pages of memory
//
// The idea is to have one page of executable trampolines,
// followed by a data page of jump target addresses.
//
// If we want to redirect a callback, only the target address needs to be
// updated, the callback function pointer can stay the same.
//
// We use mmap, so that we can be sure that our allocations
// happen at page boundaries.
#ifdef _WIN32
char* page_1 = ( char* )VirtualAlloc( NULL, page_size * 2, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE );
char* page_2 = page_1 + page_size;
if ( page_1 == NULL ) {
puts( "Error mapping page." );
return ( 1 );
}
#else
char* page_1 = mmap( NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
char* page_2 = page_1 + page_size;
if ( page_1 == MAP_FAILED ) {
puts( "Error mapping page." );
return ( 1 );
}
#endif
// --------| Invariant: mapping successful.
{
unsigned char thunk[ 8 ] = {
0xff, // Jump
0x25, // RIP-relative. Bit pattern: Mod: 00, REG: 100, R/M: 101
0xfa, // 4090 in little-endian notation, this will get patched in the next step
0x0f, // -"-
0x00, // -"-
0x00, // -"-
0x00, // Padding #1
0x00, // Padding #2
};
// Patch offset in thunk - we don't want to assume a page size.
{
int32_t* offset = ( int32_t* )&thunk[ 2 ];
*offset = int32_t( page_size ); // Corresponding entries are one page size apart
*offset -= 6; // But must subtract length of current (jmp) instruction
}
// Copy instructions for jmp into page1
for ( char* dst = page_1; dst != page_2; dst += sizeof( thunk ) ) {
memcpy( dst, thunk, sizeof( thunk ) );
}
// Write jump targets - these addresses are indirect pointers
// to the callback function.
// They start at the beginning of the second page.
void** jump_table = ( void** )( page_2 );
// Trampolines start at page_1
char* trampolines = ( page_1 );
#ifdef _WIN32
DWORD prev_protect;
bool result = true;
result = VirtualProtect( page_1, page_size, PAGE_EXECUTE_READ, &prev_protect );
assert( result );
result &= FlushInstructionCache( GetCurrentProcess(), page_1, page_size );
assert( result );
#else
// Set exec bit, but remove write bit from our plt
mprotect( page_1, page_size, PROT_READ | PROT_EXEC );
#endif
// ------ callback trampolines have been set up - let's test them
const int CALLBACK_ENTRY_INDEX = 1;
// Set forwarder for entry CALLBACK_ENTRY_INDEX to
// function `say`
jump_table[ CALLBACK_ENTRY_INDEX ] = say;
// Grab function pointer to trampoline forwarding to callback entry 1
// We make it a const function pointer to emphasise that it can't change,
// but you don't have to.
say_fun_t* const say_callback = ( say_fun_t* )( trampolines + 8 * CALLBACK_ENTRY_INDEX );
// Let's pretend the caller calls the (original) callback
say_callback( "Hello" );
// Now, let's change the callback's target address
// without telling the caller:
jump_table[ CALLBACK_ENTRY_INDEX ] = shout;
// Let's pretend the caller calls the callback again:
// this time the new method gets invoked.
say_callback( "Hello again." );
}
// Note that this unmaps page 2 as well.
#ifdef _WIN32
bool result = VirtualFree( page_1, 0, MEM_RELEASE ); // size must be zero
if ( false == result ) {
puts( "there was an error while unmapping." );
return ( 1 );
}
#else
int result = munmap( page_1, page_size * 2 );
if ( result == -1 ) {
puts( "there was an error while unmapping." );
return ( 1 );
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment