Control Flow Analysis
R5AC is planting control flow enumeration helpers into important game mode.
Just because a module is signed, doesn't mean that R5AC will give it a pass.
It follows a strict whitelist which at the time of this writing, consisted of the following ranges.
| r5apex_dx12.exe | (+1000 → +13d2000) ; '.text' |
| KERNEL32.DLL | (+1000 → +7f4bb) ; '.text' |
| ntdll.dll | (+1000 → +119f1e) ; '.text' |
| ntdll.dll | +11a000 → +11a592) ; 'PAGE' |
| ntdll.dll | (+11b000 → +11b1f9) ; 'RT' |
| steamnetworkingsockets.dll | (+1000 → +2f63a8) ; '.text |
| steam_api64.dll | (+1000 → +243de) ; '.text' |
| EOSSDK-Win64-Shipping.dll | (+1000 → +d8117c) ; '.text |
An example of this instance of detection can be found in C_ViewRender::GetMostRecentClipTransform.
__int64 C_ViewRender::GetMostRecentClipTransform(__int64 a1, char frameIndex)
{
v152 = frameIndex; // Prime RNG (seems unrelated to stack walker)
R5AC::Prime = 214013 * R5AC::Prime + 2531011; // Early exit: check stack walker enablement
if (!R5AC::IsStackWalkerEnabled)
return *(_QWORD *)(a1 + 8i64 * v152 + 1155920); __int64 stackFrames[2] = { 0 };
__int128 stackData[2] = { 0 }; unsigned short numFrames = R5AC::Imp::RtlCaptureSBT(1, 5, stackData);
if (!numFrames)
return *(_QWORD *)(a1 + 8i64 * v152 + 1155920); // Compute simple hash of the captured stack
int hashA = 5381, hashB = 0;
char* p = reinterpret_cast<char*>(stackData);
for (size_t i = 0; i < 8 * numFrames; ++i)
{
unsigned char val = *p++;
hashA = val + 33 * hashA;
hashB = val + 65599 * hashB;
}
int stackHash = hashA ^ hashB; // Lock to check hash
MEMORY[0x7FFE8D571760](&R5AC::SoemLock, 0);
char hasChanged = R5AC::HashmapHasChangedData((__int64)&R5::HashedCodeExecutionFrames, &stackHash);
MEMORY[0x7FFE8D571920](&R5AC::SoemLock); if (hasChanged != -1)
{
// If hash is known, return cached transform
return *(_QWORD *)(a1 + 8i64 * v152 + 1155920);
} // Iterate captured frames
for (unsigned int i = 0; i < numFrames; ++i)
{
__int64 frameAddr = *reinterpret_cast<__int64*>(stackData + i);
if (!frameAddr)
break; // Check if frame is in whitelisted regions
bool inWhitelist = false;
for (int j = 0; j < R5AC::WhitelistedSectionCount; ++j)
{
_WHITELISTED_SEGMENT_* region = &R5AC::WhitelistedSections[j];
if (frameAddr >= region->Begin && frameAddr <= region->End)
{
inWhitelist = true;
break;
}
} if (inWhitelist)
continue; // If not whitelisted, fetch mapped module and push violation
char moduleName[272] = { 0 };
((void(__fastcall*)(__int64, __int64*, char*, __int64))0)(-1, frameAddr, moduleName, 260); if (moduleName[0])
{
R5AC::PushViolation(nullptr, 1, frameAddr, reinterpret_cast<__int64>(moduleName));
return *(_QWORD *)(a1 + 8i64 * v152 + 1155920);
}
} // If no violations, store new hash
MEMORY[0x7FFE8D5790A0](&R5AC::SoemLock, R5AC::WhitelistedSectionCount, numFrames, R5AC::WhitelistedSections);
sub_2345E0(&R5::HashedCodeExecutionFrames, &stackHash);
MEMORY[0x7FFE8D562C70](&R5AC::SoemLock); return *(_QWORD *)(a1 + 8i64 * v152 + 1155920);
}
A little fun Fact
Some cheat developers have actually been using the aforementioned functionality against apex. See, because the API respawn uses for stack walking is resolved into an unchecked pointer in .data, anyone can just swap it out and fake the back trace buffer, or even cheaper: pretend like there's none.
