Last active
February 5, 2023 05:12
-
-
Save stevemk14ebr/12d70ae90175e71194520e9bb0c5e1e5 to your computer and use it in GitHub Desktop.
Get vtable index by parsing jump stub
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
// we're simply parsing the assembly of the jump stubs created by the compiler. The assembly encodes the displacement needed to jmp | |
// to the virtual function relative to the vtable start. If we parse this displacement out then divide by the pointer width, we | |
// can recover the index of a virtual function in a vtable. | |
template<typename T> | |
std::optional<uint16_t> getVtableIdx(T func) | |
{ | |
// this is not safe to do by the standard. | |
// however, most compilers respect it and gen expected code | |
union { | |
T pfn; | |
unsigned char* pb; | |
}; | |
pfn = func; | |
if (!pb) | |
return {}; | |
/* | |
It's common that function pointers (virtual or not) have a top level jmp. This jmp then | |
points to a stub which will either directly jmp to the virtual function | |
via one of a few encodings (if the compiler could devirtualize the call) OR | |
will dereference the object pointer to get the vtable and jmp to an offset in | |
the vtable which is the virtual function. | |
*/ | |
unsigned char* pb2 = pb; | |
#ifdef _WIN64 | |
if (pb[0] == 0xE9) { // jmp 0xNNNN1 | |
pb2 = pb + *((int32_t*)(pb + 1)) + 5; | |
} | |
if (pb2[0] == 0x48 && pb2[1] == 0x8b && pb2[2] == 0x01) { // mov rax, qword ptr [rcx] | |
if (pb2[3] == 0xff && pb2[4] == 0x20) { // jmp qword ptr [rax] | |
return (uint16_t)0; | |
} | |
else if (pb2[3] == 0xff && pb2[4] == 0x60) { // jmp qword ptr [rax + 0xNN] | |
return (uint16_t)(pb2[5] / sizeof(uint64_t)); | |
} | |
else if (pb2[3] == 0xff && pb2[4] == 0xa0) { // jmp qword ptr [rax + 0xNNNNNNNN] | |
return (uint16_t)(*((int32_t*)(pb2 + 5)) / sizeof(uint64_t)); | |
} | |
} | |
#else | |
int32_t pboff = -1; | |
if (pb[0] == 0xE9) { // jmp 0xNNNN1 | |
pb2 = pb + *((int32_t*)(pb + 1)) + 5; // e9 jmp is 5 bytes in size | |
} | |
if (pb2[0] == 0x8b && pb2[1] == 0x01) { //mov eax, [ecx] | |
pboff = 2; | |
} | |
else if (pb2[0] == 0x8b && pb2[1] == 0x44 && pb2[2] == 0x24 && pb2[3] == 0x04 && //mov eax, [esp+arg0] | |
pb2[4] == 0x8b && pb2[5] == 0x00) { //mov eax, [eax] | |
pboff = 6; | |
} | |
if (pboff > 0) { | |
if (pb2[pboff] == 0xff) { | |
switch (pb2[pboff + 1]) { | |
case 0x20: //jmp dword ptr [eax] | |
return (uint16_t)0; | |
case 0x60: //jmp dword ptr [eax+0xNN] | |
return (uint16_t)((((int32_t)pb2[pboff + 2]) & 0xff) / sizeof(uint32_t)); | |
case 0xa0: //jmp dword ptr [eax+0xNNN] | |
return (uint16_t)((*(uint32_t*)(pb2 + (pboff + 2))) / sizeof(uint32_t)); | |
default: | |
break; | |
} | |
} | |
} | |
#endif | |
// Add the case to the code above, it's possible some compiler generates a case we didn't handle yet. Just go diassemle the stub, | |
// figure out what the asm is encoding, compare that to the index of the virtual function, then write a parsing case above to handle it. | |
assert(false); | |
return {}; | |
} | |
// example use, pass address of virtual function: | |
if (auto maybe_idx = getVtableIdx(&IWbemClassObject::Get)) { | |
const uint16_t vtable_idx = *maybe_idx; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Written to handle the patterns of MSVC codegen.