How Roblox used Windows Hotpatching to log exploiters

117 views c++ ida reverse-engineering

by CPunch

This will be a short post but I wanted to share this as I thought it was pretty neat and people could learn from it. I've been looking at a Roblox Client from 2016, and it's security. Today I wanted to talk about something I recently noticed, and a little bit about how Windows has changed over the years.


Now if you've been in the exploiting scene at all you've probably seen this copy and pasted in a lot of leaked or opensource sources.


void CreateConsole() 
{
   DWORD consoleOldProtect = 0;
   VirtualProtect(FreeConsole, 1, PAGE_EXECUTE_READWRITE, &consoleOldProtect);
   *(UINT*)FreeConsole = 0xC3;
   AllocConsole();
   freopen("CONOUT$", "w", stdout);
   freopen("CONIN$", "r", stdin);
}


But what exactly does this do? Well, if you were to just AllocConsole() without that first part your console would suddenly disappear! It turns out, the MCC (MemoryCheckerChecker, whose job is to check that the MemoryChecker is still running) also calls FreeConsole every few seconds! This bypass takes advantage of a windows feature called the Hotpatch Prolog. The hotpatch prolog was a feature of the Windows OS, and it was originally meant to allow updating of functions during runtime without having to restart. The prolog itself is a two byte nop, a MOV EDI, EDI instruction. Because of this functionality it would make patching WinAPI for a process extremely easy.



So what this copy/pasted code does is replace the first byte of the two byte nop with a RET instruction (or 0xC3 in hex). This means any function that calls FreeConsole will immediately return, making the function do nothing lol. However, the Roblox client actually checks for this! After it calls FreeConsole() it will also check the hotpatch prolog for any modifications! The actual code in the MemoryCheckerChecker that does this looks something like this:


if (someFFlag)
{
    FreeConsole();
}
if (*reinterpret_cast<uint16_t*>(&FreeConsole) != 0xFF8B)
{
    // add to report to send to server
}


This checks FreeConsole for it's hotpatch prolog, and if it is missing, it'll populate a report and send it to the server. Now, luckily for us the 2 byte prolog is just a 2 byte nop, meaning it doesn't do anything. We can actually just leave that prolog there, and modify the next byte right after that. Here's my pseudo code for that using my QuickHook library I made a while ago. I generated the bytecode using an online assembler.


[I did the hotpatch + ret for my shellcode]


// patch FreeConsole with the 2 byte nop and RET so we can open a console. this also bypasses their "hook check" where they only look for the hotpatch prolog (the 2 byte nop)
BYTE patch[3] = {0x8B, 0xFF, 0xC3};
QPatch freeconPatch((void*)&FreeConsole, patch, (size_t)3);
freeconPatch.patch();

// creates a console for stuff
AllocConsole();
FILE* pCout;
freopen_s(&pCout, "CONOUT$", "w", stdout);
freopen_s(&pCout, "CONIN$", "r", stdin);

std::cout << "-Patched FreeConsole and called AllocConsole successfully!" << std::endl;

// verifies hotpatch prolog is still intact
if (*reinterpret_cast<uint16_t*>(&FreeConsole) != 0xFF8B)
{
    std::cout << "WARNING: Hotpatch can be detected by ROBLOX!!" << std::endl;
}


This will take care of the patching and open a console for you, and I also used freopen_s so your compiler won't scream at you, you're welcome. However if you don't want to include a header in your project (understandable) you can always just ignore the hotpatch prolog completely and just write at an offset of 2. So a revised version of that CreateConsole function would look like:


void CreateConsole() 
{
   // patch FreeConsole
   DWORD consoleOldProtect = 0;
   VirtualProtect(((DWORD*)FreeConsole) + 2, 1, PAGE_EXECUTE_READWRITE, &consoleOldProtect);
   *(UINT*)(((DWORD*)FreeConsole) + 2) = 0xC3;

   // Open new console
   AllocConsole();
   freopen("CONOUT$", "w", stdout);
   freopen("CONIN$", "r", stdin);
}


HOWEVER after I had already wrote that, I had realized that Windows actually no longer has a hotpatch enabled Kernel32.dll! Windows 10 ships kernel32.dll without the /hotpatch flag, which for some reason I couldn't find anyone talking about?? If we take a look at it in IDA Pro we can see these instructions are missing!



This is probably what made ROBLOX remove the check in later versions. Windows probably removed this feature because it wasn't being used, and really only allowed jumps 127 bytes in absolute length. While it is sad to see this feature removed, it doesn't make logical sense to keep this feature when all it really does is bloat the executable.


While this might not help anyone today, it's definitely very interesting to see. This means that they could have probably silently logged thousands of people, all because of skids who copy and paste. Doing your own research (even if you already know what it does) can be rewarding because someone might've overlooked something.


Oct 25, 2019 by CPunch