# R5AC: Apex Legends anti-cheat Analysis (S25)

An analysis of respawn entertainment's R5AC for Apex Legends Season 25.

# Introduction

Learn about how respawn improved client-side cheat detection in Apex Legends.

# Specification

<div class="flex max-w-full flex-col grow" id="bkmrk-"><div class="min-h-8 text-message relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal [.text-message+&]:mt-1" data-message-author-role="assistant" data-message-id="e43f85e3-db50-490f-b1d0-de48c64d6fdc" data-message-model-slug="gpt-5-2" dir="auto">  
</div></div><span style="color: rgb(255, 255, 255);">R5AC is the name of an in-house cheat detection software. It seems to be made by a team at respawn entertainment, although there is no public information about it anywhere on the internet.</span>

### <span style="color: rgb(255, 255, 255);">Where is it?</span>

<span style="color: rgb(255, 255, 255);">It is located in the main game executable, r5apex\_dx12.exe. Sometimes you will encounter entire functions that will be related to it, other times it'll be some inlined code in some important game/engine functions. It uses a basic xor transform on it's c-strings, which makes it so that it will only be decrypted on the stack. However, it's quite easy to statically analyze a runtime dump of Apex Legends, and figure out a way to:</span>

<div class="flex max-w-full flex-col grow" id="bkmrk-find-all-instances-o"><div class="min-h-8 text-message relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal [.text-message+&]:mt-1" data-message-author-role="assistant" data-message-id="e43f85e3-db50-490f-b1d0-de48c64d6fdc" data-message-model-slug="gpt-5-2" dir="auto"><div class="flex w-full flex-col gap-1 empty:hidden first:pt-[1px]"><div class="markdown prose dark:prose-invert w-full wrap-break-word dark markdown-new-styling">1. <span style="color: rgb(255, 255, 255);">Find all instances of encrypted R5AC C-String's, preferably automated.</span>
2. <span style="color: rgb(255, 255, 255);">Figure out the following parameters for this transformation:</span>
    
    
    - <span style="color: rgb(255, 255, 255);">Location of the encrypted data.</span>
    - <span style="color: rgb(255, 255, 255);">Location of the encryption key.</span>
    - <span style="color: rgb(255, 255, 255);">Length of the encrypted data.</span>
3. <span style="color: rgb(255, 255, 255);">In case of this cheat detection software, the length of an encrypted C-String's encoded data equals it's encryption key's size. This might be done to prevent repeating keys during the XOR transform, which can weaken the overall effectiveness of encryption.</span>

</div></div></div></div>### <span style="color: #ffffff;">What are it's capabilities?</span>

<span style="color: #ffffff;">Generally speaking, this specific solution seems to focus on the game process and it's local process context. It mostly features detections for internal cheats. These detections range from heurstics all the way down to very specific, concrete signals.</span>

# Obfuscation: Constant (C-String)

R5AC uses a simple XOR algorithm where the decryption key length and encrypted content length are identical.

## Referenced &amp; encoded C-Strings Analysis

### DLL Related

> 0x20ea35 KERNEL32.dll  
> 0x3a3e51 ntdll.dll  
> 0x54ee61 ADVAPI32.dll  
> 0x542551 steamnetworkingsockets.dll  
> 0x542691 steam\_api64.dll  
> 0x2f1cc2 EOSSDK-Win64-Shipping.dll  
> 0x54ee31 EasyAntiCheat\_EOS  
> 0x3a3e81 wine\_get\_version

### **API Related**

> **0x20ea60 K32GetMappedFileNameA  
> 0x20ee00 VirtualQuery  
> 0x20f160 GetLastError  
> 0x7df831 VirtualProtect  
> 0x54ee91 OpenSCManagerA  
> 0x54eec1 OpenServiceA  
> 0x54eef1 QueryServiceStatusEx  
> 0x54ef21 CloseServiceHandle  
> 0x543471 WideCharToMultiByte  
> 0x541fc0 RtlCaptureStackBackTrace**

### **Control Flow Analysis / Game Functions**

> **0x20e9f0 CS\_CEngineClient::Engine\_SetViewAngles  
> 0x26c540 CS\_CNetChan::SetTimeout  
> 0x26dfe0 CS\_CNetChan::SendReliableMessages  
> 0x270511 CS\_CNetChan::SendDatagram  
> 0x2744c1 CS\_CNetChan::SendNetMsg  
> 0x275210 CS\_CNetChan::SendData  
> 0x3ae540 CS\_Playlist\_GetPlaylistVar  
> 0x7cf601 CS\_C\_BaseEntity::CalcAbsoluteVelocity  
> 0x878620 CS\_CViewRender::GetMostRecentClipTransform  
> 0x951be1 CS\_CInput::Input\_CreateMove  
> 0x952811 VTP\_CInput::Input\_CreateMove  
> 0x59ebd0 CS\_CCommandBuffer::AddText  
> 0x69b830 CS\_UTIL\_TraceRay\_Client  
> 0xa58200 CS\_C\_BaseEntity::GetEntityNameAsCStr  
> 0xabe800 CS\_C\_MoveData::MoveData\_Init  
> 0xcc4e10 CS\_Pak\_RequireSignedPaks  
> 0xcc8c51 CS\_Pak\_ValidateSignatureForCurrentReadingFile  
> 0xcd89a1 CS\_WrappedFileSystem\_Open**

### **Virtual Method Table Analysis**

> **0x22ee01 VTP\_GetEngineTraceClient  
> 0x22f271 VTP\_GetEngineTraceClientDecals  
> 0x479351 VTP\_GetFilesystemInterface  
> 0x817371 VTP\_C\_Player::Spawn  
> 0x865491 VTP\_GetEntityList  
> 0x9f13b1 VTP\_GetViewRenderInstance**

### Miscellaneous

> 0x20f0f0 BA:0x%llX AB:0x%llX RS:0x%llX AP:0x%lX PR:0x%lX TY:0x%lX  
> 0x5427e0 Callstack Init Failed  
> 0x543821 os\_version  
> 0x543a12 gpu\_vendor  
> 0x543c91 render\_device\_driver\_version  
> 0x543e81 cpu\_brand  
> 0x544042 windows\_install\_date  
> 0x544205 is\_wine  
> 0x544231 0  
> 0x544260 1  
> 0x544780 language  
> 0x544a40 HWID\_%02X-%02X  
> 0x544ab0 %02X  
> 0x544db1 HWID\_%02X-FAILURE  
> 0x54a0b1 %u:%u:%X:%llX:%llX:%llX  
> 0x54fa91 Startup  
> 0x7dfb91 remove permissions

# Anti-Cheat Networking

Understanding how an anti-cheat communicates will help you navigate around it more efficiently.

# Client>Server Communication

There is no singular function that is responsible for this task. In Apex, we rather have multiple of those working together.

Let's get started with the most common function, **R5AC::PushViolation:**

> **void \_\_fastcall R5AC::PushViolation(  
>  const \_\_m128i \*pszIdentifierStr,  
>  char Severity,  
>  \_\_int64 ExtraDataPtr,  
>  \_\_int64 ExtraDataLen);**

In case of client-side anomalies being detected, it will be invoked like this:

> **LBL\_ON\_ABNORMALITY\_FOUND:  
>  v119 = v116();  
>  v120 = 0;  
>  v121 = 0i64;  
>  do  
>  {  
>  v122 = \*(\_BYTE \*)(v121 + 41489128); // these are just data and key, and the RVA can be computed with analysis of a runtime dump.  
>  ++v120;  
>  v123 = \*(\_BYTE \*)(v121 + 27928337);  
>  v188\[++v121 + 15\] = v123 ^ v122;  
>  }  
>  while ( v120 &lt; 0xC ); // C-String encryption using simple xor  
>  sprintf\_s\_2(Buffer, 0x104ui64, Format, v119);  
>  }  
>  }  
>  R5AC::PushViolation(v200, 1, v177, (\_\_int64)Buffer);**

It's purpose is to enqueue a new violation record into a list that is fetched and emptied by numerous callers, all originating from **r5apex\_dx12.exe.** These are to be considered slaves because they simply check if there is any messages pending, use CLC\_AntiCheat virtual method table to setup a new netmessage in source engine, and then puts the data of the anti-cheat violation/message into the netmsg and sends it to the game server.

> if ( gpNetChan )  
>  {  
>  v14 = vft::CLC\_AntiCheatMsg;  
>  v15 = 0;  
>  v17 = 0i64;  
>  v5 = 5;  
>  v16 = 1;  
>  // send all of them in bulk  
>  for ( i = 0; (unsigned \_\_int8)R5AC::PopAnticheatMsg(v20, &amp;i); --v5 )  
>  {  
>  if ( !v5 )  
>  break;  
>  v18 = v20;  
>  v19 = i;  
>  C\_NetChan::SendNetMsg(v4, &amp;v14, 0, 0);  
>  C\_NetChan::SendDatagram(v4, 0i64);  
>  }  
>  }

Additionally, there is a couple more ways for the anti-cheat to phone back home. Let me introduce you **R5AC::TrySendViolationOrQueue.** It behaves almost identically to it's predecessor, with the twist that if your game client has an active network channel instance (or in other words: an active connection to a game server is present), it will bypass it's usual queue-based system and directly send it to the game server.

> <div><div>void __fastcall R5AC::TrySendViolationOrQueue(</div><div> const __m128i *id_str,</div><div> __int64 a2,</div><div> __int64 extraData,</div><div> const __m128i *extraDataLen)</div><div>{</div><div> --*(_DWORD *)(v4 + 68);</div><div> v5 = gpNetChan;</div><div> if ( gpNetChan &amp;&amp; (unsigned int)MEMORY[0x7FFE8D2B5550](id_str, 0i64) == dword_594427C )</div><div> {</div><div> v28 = 0;</div><div> v9 = id_str;</div>  
> <div> do</div><div> {</div><div> v10 = v9-&gt;m128i_i8[0];</div><div> v9-&gt;m128i_i8[pszBuffer - (char *)id_str] = v9-&gt;m128i_i8[0];</div><div> v9 = (const __m128i *)((char *)v9 + 1);</div><div> }</div><div> while ( v10 );</div>  
> <div> idStrLength = -1i64;</div><div> do</div><div> ++idStrLength;</div><div> while ( id_str-&gt;m128i_i8[idStrLength] );</div><div> v12 = &amp;pszBuffer[idStrLength + 1];</div><div> if ( extraDataLen )</div><div> {</div><div> idx = -1i64;</div><div> do</div><div> ++idx;</div><div> while ( extraDataLen-&gt;m128i_i8[idx] );</div><div> R5::AllocAndCopyStruWithHdr(</div><div> &amp;mem,</div><div> extraDataLen,</div><div> idx,</div><div> (__int64 (__fastcall **)(_QWORD, unsigned __int64, __int64))off_1AA6728);</div><div> extraDataPtr = mem;</div><div> }</div><div> else</div><div> {</div><div> extraDataPtr = 0i64;</div><div> mem = 0i64;</div><div> }</div><div> *(_DWORD *)v12 = 1;</div><div> v12[4] = 0;</div><div> *(_QWORD *)(v12 + 5) = extraData;</div><div> sse_memcpy((__m128i *)(v12 + 13), extraDataPtr, (unsigned int)(extraDataPtr[-1].m128i_i32[3] + 1));</div><div> v15 = (_DWORD)v12 - ((unsigned int)&amp;v27 + 880) + 14 + extraDataPtr[-1].m128i_i32[3];</div><div> someKey = 0x42A1110F96B5E116i64;</div><div> v16 = 0;</div><div> if ( v15 != 2 )</div><div> {</div><div> v17 = pszBuffer;</div><div> do</div><div> {</div><div> ++v17;</div><div> v18 = v16++ &amp; 7;</div><div> *(v17 - 1) ^= *((_BYTE *)&amp;someKey + v18);</div><div> }</div><div> while ( v16 &lt; v15 - 2 );</div><div> }</div><div> v26 = v15;</div><div> *(_QWORD *)&amp;v21 = vft::CLC_AntiCheatMsg;</div><div> v22 = 0;</div><div> v25 = &amp;v28;</div><div> v24 = 0i64;</div><div> v23 = 1;</div><div> C_NetChan::SendNetMsg(v5, &amp;v21, 0, 0);</div><div> C_NetChan::SendDatagram(v5, 0i64);</div><div> sse_memset((__int64)&amp;v28, 0, 0x400ui64);</div><div> *(_QWORD *)&amp;v21 = &amp;off_173A878;</div><div> if ( _InterlockedExchangeAdd(&amp;extraDataPtr[-1].m128i_i32[2], 0xFFFFFFFF) == 1 )</div><div> (*(void (__fastcall **)(__int64))(extraDataPtr[-1].m128i_i64[0] + 8))(extraDataPtr[-1].m128i_i64[0]);</div><div> }</div><div> else</div><div> {</div><div> R5AC::PushViolation(id_str, 0, extraData, (__int64)extraDataLen);</div><div> }</div><div>}</div></div>

<div id="bkmrk--3"></div>> 

# Basic Networking Specification

In general, R5AC seems to only make use of it's networking capabilities when it detects that something might be wrong.

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

# Hardware ID

# Does R5AC have hardware fingerprinting capabilities?

Not directly, but Apex Legends itself additionally uses **Theia** for a second form of machine fingerprinting. It doesn't replace EAC's mechanisms for the same thing, but rather seems to work alongside it. As an additional vector, so to speak.

The game will build a string that seems to resemble a hardware ID. I haven't reconstructed it fully yet, which is why this page is to be considered work in progress.

I've seen routines in both apex legends itself, but also theia's runtime which seem to be responsible for doing stuff with hardware identification. It seems like theia's runtime is doing all the heavy lifting, as i was able to trace a lot of NtDeviceIoControlFile calls from some suspicious region in apex legends. Of course it was the theia runtime. It led me to multiple giant functions which used various IOCTL codes and addressed different devices. Name of the devices were encrypted using some inline transformation in the code. But it seems fairly trivial to decrypt.

When i'll have some free time again, i will verify which codes are actually really queried in a live system.