# Anti-Cheat Detections

# Virtual Method Tables

R5AC is ensuring that VMTP's are pointing within expected bounds, and that read-only VMT related data has not been tampered with.

---

# Pointer Verification

Every time the script calls `sub_1DCDD1`, the virtual method table pointer (VMTP) for the filesystem interface is verified by `VTP_GetFilesystemInterface`. This function performs an integrity check to ensure the interface has not been tampered with or injected. There are numerous other variants of this VMTP related detection, all on different interfaces and virtual method tables, or object instances.

## How the detection works

1. **VMTP location check**
    
    
    - The function calculates the address of `FilesystemInterfaceVMTP` and checks whether it lies within a valid PE section of the main game module.
    - If the pointer is **outside expected sections**, this indicates possible tampering or injection.
2. **Module and export verification**
    
    
    - It scans the loaded modules for `KERNEL32.DLL` and finds the export `K32GetMappedFileName`.
    - This function is used to retrieve the **file path of the module** where the VMTP resides.
3. **Violation reporting**
    
    
    - If the VMTP is outside a legitimate module section, `R5AC::PushViolation` is called.
    - This flags a potential anti-cheat violation.

<p class="callout warning">In more recent builds, R5AC has started verifying that the backing segment of the VMTP's linear address are having a plausible characteristics. it must contain IMAGE\_SCN\_CNT\_INITIALIZED\_DATA/IMAGE\_SCN\_MEM\_READ</p>

---

**Conclusion:**  
`VTP_GetFilesystemInterface` is an anti-tamper routine that validates the location of the filesystem VMTP and ensures it points to legitimate code within the game’s module. Any deviation triggers a violation report, preventing injected or modified interfaces from being used.

Here is an example of how this detection might look like.

> // Everytime sub\_1DCDD1 is called, the check (VTP\_GetFilesystemInterface) is executed.
> 
> char \_\_fastcall sub\_1DCDD1()  
> {   
>  FilesystemInterface = (\_\_int64 \*)R5::VTP\_GetFilesystemInterface();   
>  \*((\_QWORD \*)v2 + 16) = FilesystemInterface;  
>  gpFileSystemInterface\[0\] = FilesystemInterface;  
>  return 1;  
> }
> 
> void \*\*R5::VTP\_GetFilesystemInterface()  
> {  
>  \_LIST\_ENTRY\* lpK32GetMappedFileName = nullptr;  
>  void\* VmtTablePtr = R5::FilesystemInterfaceVMTP;  
>  void\* VmtTablePtr2 = R5::FilesystemInterfaceVMTP;  
>  \_\_int64 ImageBaseAddress = (\_\_int64)NtCurrentTeb()-&gt;ProcessEnvironmentBlock-&gt;ImageBaseAddress;
> 
>  // Plaintext strings (replacing encrypted runtime decoding)  
>  const char ModuleNameStr\[\] = "KERNEL32.DLL";  
>  const char ExportFuncStr\[\] = "K32GetMappedFileName";  
>  const char InterfaceStr\[\] = "VTP\_GetFilesystemInterface";
> 
>  if (\*(WORD\*)ImageBaseAddress == 0x5A4D) // Check for 'MZ' PE signature  
>  {  
>  \_IMAGE\_NT\_HEADERS64\* lpNTH = (\_IMAGE\_NT\_HEADERS64\*)(ImageBaseAddress + \*(int\*)(ImageBaseAddress + 0x3C));  
>  if (lpNTH-&gt;OptionalHeader.Magic == 0x20B) // PE32+  
>  {  
>  unsigned int NumberOfSections = lpNTH-&gt;FileHeader.NumberOfSections;  
>  char\* lpFirstSectionContentStart = (char\*)&amp;lpNTH-&gt;OptionalHeader + lpNTH-&gt;FileHeader.SizeOfOptionalHeader;
> 
>  if (NumberOfSections &amp;&amp; lpFirstSectionContentStart)  
>  {  
>  \_IMAGE\_SECTION\_HEADER\* dwTableRVA = (\_IMAGE\_SECTION\_HEADER\*)((char\*)R5::FilesystemInterfaceVMTP - ImageBaseAddress);  
>  \_DWORD\* sectionHeaders = (\_DWORD\*)(lpFirstSectionContentStart + 8);  
>  unsigned int nSectionIdx = 0;
> 
>  while (true)  
>  {  
>  \_IMAGE\_SECTION\_HEADER\* dwSegmentRVA = (\_IMAGE\_SECTION\_HEADER\*)(unsigned int)sectionHeaders\[1\];  
>  if (dwSegmentRVA &lt;= dwTableRVA &amp;&amp;  
>  (unsigned int)((\_DWORD)dwSegmentRVA + \*sectionHeaders) &gt; (unsigned \_\_int64)dwTableRVA)  
>  {  
>  break; // Found containing section  
>  }
> 
>  ++nSectionIdx;  
>  sectionHeaders += 10;
> 
>  if (nSectionIdx &gt;= NumberOfSections)  
>  {  
>  // Outside module section — use default  
>  VmtTablePtr = VmtTablePtr2;  
>  goto ON\_DETECTED\_OUTSIDE\_INGAME\_SECTION;  
>  }  
>  }  
>  }  
>  }  
>  }
> 
> ON\_DETECTED\_OUTSIDE\_INGAME\_SECTION:
> 
>  char lpMappedFileName\[272\] = {0};  
>  wchar\_t NeedleForSomeModule\[264\] = {0};  
>  mbstowcs\_s(nullptr, NeedleForSomeModule, ModuleNameStr, sizeof(ModuleNameStr));
> 
>  // Search loaded modules  
>  \_LIST\_ENTRY\* p\_InMemoryOrderModuleList = &amp;NtCurrentTeb()-&gt;ProcessEnvironmentBlock-&gt;Ldr-&gt;InMemoryOrderModuleList;  
>  \_LIST\_ENTRY\* Flink = p\_InMemoryOrderModuleList-&gt;Flink;
> 
>  if (Flink != p\_InMemoryOrderModuleList)  
>  {  
>  while (\_wcsicmp(NeedleForSomeModule, (\_WCHAR\*)Flink\[5\].Flink) != 0)  
>  {  
>  Flink = Flink-&gt;Flink;  
>  if (Flink == p\_InMemoryOrderModuleList)  
>  goto ON\_DETECTED;  
>  }
> 
>  \_LIST\_ENTRY\* lpModuleBase = Flink\[2\].Flink;  
>  if (lpModuleBase)  
>  {  
> LABEL\_22:  
>  if (LOWORD(lpModuleBase-&gt;Flink) == 0x5A4D) // 'MZ'  
>  {  
>  \_\_int64 lpExportDirectory = (\_\_int64)lpModuleBase + SHIDWORD(lpModuleBase\[3\].Blink);  
>  if (\*(WORD\*)(lpExportDirectory + 24) == 0x20B) // PE32+  
>  {  
>  unsigned int\* v27 = (unsigned int\*)(lpExportDirectory + 0x88);  
>  if (!v27)  
>  v27 = nullptr;
> 
>  if (v27)  
>  {  
>  unsigned int nExportEnumIdx = 0;  
>  \_DWORD\* v29 = (\_DWORD\*)((char\*)lpModuleBase + \*v27);  
>  \_\_int64 AddressOfFunctions = (\_\_int64)lpModuleBase + (unsigned int)v29\[7\];  
>  \_\_int64 AddressOfNames = (\_\_int64)lpModuleBase + (unsigned int)v29\[8\];  
>  unsigned int NumberOfExports = v29\[6\];  
>  \_\_int64 AddressOfNameOrdinals = (\_\_int64)lpModuleBase + (unsigned int)v29\[9\];
> 
>  while (nExportEnumIdx &lt; NumberOfExports)  
>  {  
>  char\* lpExportRecordMaybe = (char\*)lpModuleBase + \*(unsigned int\*)(AddressOfNames + 4i64 \* nExportEnumIdx);  
>  if (strcmp(lpExportRecordMaybe, ExportFuncStr) == 0)  
>  {  
>  unsigned int dwFunctionRVA = \*(\_DWORD\*)(AddressOfFunctions  
> \+ 4i64 \* \*(unsigned \_\_int16\*)(AddressOfNameOrdinals + 2i64 \* nExportEnumIdx));
> 
>  lpK32GetMappedFileName = (\_LIST\_ENTRY\*)((char\*)lpModuleBase + dwFunctionRVA);  
>  break;  
>  }  
>  ++nExportEnumIdx;  
>  }  
>  }  
>  }  
>  }  
>  }  
>  }
> 
> ON\_DETECTED:
> 
>  // Get the module file name of where this VMTP is residing in.  
>  if (lpK32GetMappedFileName)  
>  ((void(\_\_fastcall\*)(\_\_int64, void\*, char\*, \_\_int64))lpK32GetMappedFileName)(  
>  -1,  
>  VmtTablePtr2,  
>  lpMappedFileName,  
>  sizeof(lpMappedFileName)  
>  );
> 
>  // Report violation if needed  
>  R5AC::PushViolation(nullptr, 2, (\_\_int64)VmtTablePtr2, (\_\_int64)lpMappedFileName);
> 
>  return &amp;R5::FilesystemInterfaceVMTP;  
> }

# 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.

<table border="1" class="highlight tab-size js-file-line-container" data-hpc="" data-paste-markdown-skip="" data-tab-size="4" data-tagsearch-path="R5AC Whitelisted Memory Regions for code execution" id="bkmrk-r5apex_dx12.exe-%28%2B10" style="border-collapse: collapse; border-style: solid; height: 245.334px; width: 67.8378%;"><tbody><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk-r5apex_dx12.exe-%28%2B10-1" style="width: 54.9238%; height: 30.6667px;">r5apex\_dx12.exe</td><td style="width: 45.1648%; height: 30.6667px;">(+1000 → +13d2000) ; '.text'</td></tr><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk-" style="width: 54.9238%; height: 30.6667px;">KERNEL32.DLL </td><td style="width: 45.1648%; height: 30.6667px;">(+1000 → +7f4bb) ; '.text'</td></tr><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk-kernel32.dll-%28%2B1000-" style="width: 54.9238%; height: 30.6667px;">ntdll.dll</td><td style="width: 45.1648%; height: 30.6667px;">(+1000 → +119f1e) ; '.text'</td></tr><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk--1" style="width: 54.9238%; height: 30.6667px;">ntdll.dll</td><td style="width: 45.1648%; height: 30.6667px;">+11a000 → +11a592) ; 'PAGE'</td></tr><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk-ntdll.dll-%28%2B1000-%E2%86%92-%2B" style="width: 54.9238%; height: 30.6667px;">ntdll.dll</td><td style="width: 45.1648%; height: 30.6667px;">(+11b000 → +11b1f9) ; 'RT'</td></tr><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk-ntdll.dll-%28%2B11a000-%E2%86%92" style="width: 54.9238%; height: 30.6667px;">steamnetworkingsockets.dll</td><td style="width: 45.1648%; height: 30.6667px;">(+1000 → +2f63a8) ; '.text</td></tr><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk-ntdll.dll-%28%2B11b000-%E2%86%92" style="width: 54.9238%; height: 30.6667px;">steam\_api64.dll</td><td style="width: 45.1648%; height: 30.6667px;">(+1000 → +243de) ; '.text'</td></tr><tr style="height: 30.6667px;"><td class="blob-code blob-code-inner js-file-line" id="bkmrk--2" style="width: 54.9238%; height: 30.6667px;">EOSSDK-Win64-Shipping.dll</td><td style="width: 45.1648%; height: 30.6667px;">(+1000 → +d8117c) ; '.text</td></tr></tbody></table>

Their whitelist is using a relatively primitive structure for describing what they deem a whitelisted memory range:

> struct WHITELISTED\_REGION
> 
> {
> 
>  u64 Begin;
> 
>  u64 End;
> 
> };

<p class="callout info">In more recent builds of **Apex Legends**, this list is now more complex to interpret and seems to have switched to using some computed hash of whitelisted module bases, and checking for that instead. R5AC only uses RtlCaptureStackBackTrace for examining control flow. </p>

<p class="callout warning">In 2026, R5AC has switched to manual stack tracing via RtlCaptureContext + RtlLookupFunctionEntry + RtlVirtualUnwind.</p>

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&lt;char\*&gt;(stackData);  
>  for (size\_t i = 0; i &lt; 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\](&amp;R5AC::SoemLock, 0);  
>  char hasChanged = R5AC::HashmapHasChangedData((\_\_int64)&amp;R5::HashedCodeExecutionFrames, &amp;stackHash);  
>  MEMORY\[0x7FFE8D571920\](&amp;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 &lt; numFrames; ++i)  
>  {  
>  \_\_int64 frameAddr = \*reinterpret\_cast&lt;\_\_int64\*&gt;(stackData + i);  
>  if (!frameAddr)  
>  break; // Check if frame is in whitelisted regions  
>  bool inWhitelist = false;  
>  for (int j = 0; j &lt; R5AC::WhitelistedSectionCount; ++j)  
>  {  
>  \_WHITELISTED\_SEGMENT\_\* region = &amp;R5AC::WhitelistedSections\[j\];  
>  if (frameAddr &gt;= region-&gt;Begin &amp;&amp; frameAddr &lt;= region-&gt;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&lt;\_\_int64&gt;(moduleName));  
>  return \*(\_QWORD \*)(a1 + 8i64 \* v152 + 1155920);  
>  }  
>  } // If no violations, store new hash  
>  MEMORY\[0x7FFE8D5790A0\](&amp;R5AC::SoemLock, R5AC::WhitelistedSectionCount, numFrames, R5AC::WhitelistedSections);  
>  sub\_2345E0(&amp;R5::HashedCodeExecutionFrames, &amp;stackHash);  
>  MEMORY\[0x7FFE8D562C70\](&amp;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 nothing on the stack was unwindable. As if that wasn't enough, respawn also have decided to making calls by referencing it's value across the game's entire code base.

![](https://i.imgur.com/KHG1cY8.png)

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!

> <div>unsigned short __fastcall hk_capture_stack_back_trace(unsigned long frames_to_skip, unsigned long frames_to_capture, void **trace, void *trace_hash)</div><div>{</div><div>auto retaddr = BASE_OF(_ReturnAddress()) - gctx-&gt;game_base;</div><div>g.r5ac_stack_walk_last_call = retaddr;</div><div>  
> </div><div>if(g.conf.anon_mode)</div><div>{</div><div>*reinterpret_cast&lt;uptr*&gt;(gctx-&gt;game_base + gctx-&gt;offsets.string_dict_user_names) = 0;</div><div>}</div><div>  
> </div><div>if (retaddr == gctx-&gt;offsets.ret_addrs.create_move)</div><div>{</div><div>g.inputs.cmove.tick_begin();</div><div>  
> </div><div>features::on_anonymizer();</div><div>if (is_valid_session())</div><div>{</div><div>auto me = local_player();</div><div>if (me.is_alive())</div><div>{</div><div>if (g.conf.rcs_strength &gt; 0)</div><div>{</div><div>float rcs_strength = (static_cast&lt;float&gt;(g.conf.rcs_strength) / 1000.f);</div><div>g.conf.rcs_internal_mouse_scale = rcs_strength;</div><div>}</div><div>  
> </div><div>if (g.conf.aim_assist_strength &gt; 0)</div><div>{</div><div>float aa_strength = (static_cast&lt;float&gt;(g.conf.aim_assist_strength) / 100.f);</div><div>g.conf.aim_assist_internal_mouse_scale = (gfloats.one - aa_strength);</div><div>}</div><div>  
> </div><div>features::on_movement(me);</div><div>features::on_trigger(me);</div><div>}</div><div>}</div><div>  
> </div><div>g.inputs.cmove.tick_end();</div><div>}</div><div>  
> </div><div>return 0; /* no traces are available */</div><div>}</div>

> bool disarm\_stack\_walker()  
>  {  
>  size\_t num\_handled = 0;  
>  uint64\_t needle = gctx-&gt;winapis.capture\_stack\_back\_trace;  
>  if (needle)  
>  {  
>  uintptr\_t offset = 0;  
>  auto data = (u8\*)gctx-&gt;game\_data\_base;  
>  auto end = (u8\*)(gctx-&gt;game\_data\_base + gctx-&gt;game\_data\_len);
> 
>  while (true)  
>  {  
>  auto result = stl::search(data + offset, end, (uint8\_t \*)&amp;needle, sizeof(uintptr\_t));  
>  if (result != 0)  
>  {  
>  #if LOGGING\_IS\_ENABLED == 1  
>  LMsg(256, "\[R5AC\] disarming stack walk at index %i and pointer %p", num\_handled, result);  
>  #endif  
>  \*reinterpret\_cast&lt;void \*\*&gt;(result) = hk\_capture\_stack\_back\_trace;  
>  ++num\_handled;  
>  offset = result - (uintptr\_t)data + 1;  
>  }  
>  else  
>  {  
>  break; // No more occurrences found  
>  }  
>  }  
>  }
> 
>  g.r5ac\_num\_stack\_walks\_disarmed = num\_handled;
> 
>  return (num\_handled &gt; 0);  
>  }

Above code was tested on retail apex legends, on both windows and linux. Since the flaw is in R5AC itself, platform mostly does not matter.

<div id="bkmrk--4"></div><div id="bkmrk--5"></div>