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.

What makes this much worse is the fact that this vulnerable pointer is referenced by numerous critical game functions. It's literally a backdoor so handy that you don't even need to hook CreateMove anymore, for example. You can just swap this pointer, check if caller (return address) is C_Input::CreateMove, and and that's it!
unsigned short __fastcall hk_capture_stack_back_trace(unsigned long frames_to_skip, unsigned long frames_to_capture, void **trace, void *trace_hash){auto retaddr = BASE_OF(_ReturnAddress()) - gctx->game_base;g.r5ac_stack_walk_last_call = retaddr;if(g.conf.anon_mode){*reinterpret_cast<uptr*>(gctx->game_base + gctx->offsets.string_dict_user_names) = 0;}if (retaddr == gctx->offsets.ret_addrs.create_move){g.inputs.cmove.tick_begin();features::on_anonymizer();if (is_valid_session()){auto me = local_player();if (me.is_alive()){if (g.conf.rcs_strength > 0){float rcs_strength = (static_cast<float>(g.conf.rcs_strength) / 1000.f);g.conf.rcs_internal_mouse_scale = rcs_strength;}if (g.conf.aim_assist_strength > 0){float aa_strength = (static_cast<float>(g.conf.aim_assist_strength) / 100.f);g.conf.aim_assist_internal_mouse_scale = (gfloats.one - aa_strength);}features::on_movement(me);features::on_trigger(me);}}g.inputs.cmove.tick_end();}return 0; /* no traces are available */}
- Above code was actually used in production by me, back when apex legends was still playable on linux. If you thought that this only worked on linux: Nope. It's entirely platform independant, because the flaw is in respawn's anti-cheat and not some OS.