Skip to content

Instantly share code, notes, and snippets.

@stevemk14ebr
Last active February 5, 2023 05:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevemk14ebr/12d70ae90175e71194520e9bb0c5e1e5 to your computer and use it in GitHub Desktop.
Save stevemk14ebr/12d70ae90175e71194520e9bb0c5e1e5 to your computer and use it in GitHub Desktop.
Get vtable index by parsing jump stub
// 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;
}
@stevemk14ebr
Copy link
Author

Written to handle the patterns of MSVC codegen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment