-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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