Parts of this project have been reverse engineered and reconstructed from R5AC, an in-house anticheat solution currently deployed in APEX LEGENDS. I have simplified some of their checks due to time constrains, but behavior should be identical. I have written a few rather small tests to confirm this, which execute a function monitored by a replicated R5AC stackwalk, which intentionally do following things:
- call into monitored function from legitimate place (e.g from within the main executable)
- call into monitored function from code residing in a manually allocated RWX page.
- call into monitored function with 3 open-source return address spoofers. (gadgets used: jmp, add rsp; ret, int3)
Make sure to compile in x64 Release!
------( trying to call from process itself... )------
<<EXTRA>> [INFO] CALLED BY 'r5sw.exe'+0xae65
-----------------------------------------------------
------( trying to call from RWX page... )------
<<EXTRA>> [FLAG//////UNBACKED CODE EXECUTION(PRIMARY)] caller is originated within non-module memory.
<<EXTRA>> [INFO] CALLED BY 'UNK_be1d4ff540'
[FLAG//////UNBACKED CODE EXECUTION(SECONDARY)] suspicious record at 0 (0000022AB3B30017)
-----------------------------------------------------
------( trying to call with spoof (namazso): )------
<<EXTRA>> [INFO] CALLED BY 'r5sw.exe'+0x1000
[FLAG//////RETADDR SPOOFER] suspicious record at 0 (00007FF703471000)
-----------------------------------------------------
------( trying to call with spoof (beakers): )------
<<EXTRA>> [INFO] CALLED BY 'kernel32.dll'+0x18f24
[FLAG//////RETADDR SPOOFER] suspicious record at 0 (00007FFE230B8F24)
[FLAG//////RETADDR SPOOFER] suspicious record at 1 (00007FFE230D5B34)
[FLAG//////RETADDR SPOOFER] suspicious record at 2 (00007FFE230B5FB6)
[FLAG//////RETADDR SPOOFER] suspicious record at 3 (00007FFE230FEC99)
[FLAG//////RETADDR SPOOFER] suspicious record at 4 (00007FF70347D43B)
-----------------------------------------------------
------( trying to call with spoof (ReaP): )------
[!] spoofers::reap::VectoredHandler: Old return address: 7ff70347ba2f
[!] spoofers::reap::VectoredHandler: New return address: 7ffe2378248f
<<EXTRA>> [INFO] CALLED BY 'user32.dll'+0x7248f
[FLAG//////RETADDR SPOOFER] suspicious record at 0 (00007FFE2378248F)
[!] spoofers::reap::VectoredHandler: Returning back to 00007FF70347BA2F...
-----------------------------------------------------
Currently they use an API for generating a backtrace recording. It's located in kernel32.dll
and named RtlCaptureStackBackTrace
.
These checks are riddled around the game's normal code and you will eventually call into them.
If your cheat generates a CALL instruction on anything that later on may land into a stackwalk check, your cheat module may be exposed because by generating aforementioned CALL, you are pushing a return address to the stack which will obviously point to your cheat if you want it to return normally. There are ways to overcome this, but this isn't a bypass repository.
You should always keep in mind that it's not hard for an anticheat to detect an anomaly here, if you add the fact that all of this comes from a module that isn't even in LDR nor signed/in a whitelist, then you definitely know something's up.
It pretty much detects certain variations of gadgets commonly used when doing anything with return address spoofing, i have included a couple of open-source projects to demonstrate the detection:
- https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html
- https://www.unknowncheats.me/forum/anti-cheat-bypass/512002-x64-return-address-spoofing.html
- https://github.com/Peribunt/Exception-Ret-Spoofing/
All credits for these go to the corresponding authors. They are just being shipped for demonstration purposes.
Currently, they use following logic for what i assume, is for detecting gadgets commonly used for this purpose:
if( !tools::retaddr_is_call_insn(retaddr) )
{
// IF THIS RETURN ADDRESS HAS NOT BEEN GENERATED BY A CALL INSTRUCTION,
// PERFORM ADDITIONAL ANALYSIS TO DETERMINE IF A RETURN ADDRESS SPOOFER WAS POTENTIALLY USED.
DWORD_PTR v50 = BASE_OF(retaddr);
DWORD_PTR v57 = 2i64;
while (*(BYTE*)(v50 - v57) != 0xFF || (((*(BYTE*)(v50 - v57 + 1) & 0x38) - 16) & 0xF7) != 0) // <------------------------------- see here
{
if (++v57 > 7)
{
printf("[FLAG//////RETADDR SPOOFER] suspicious record at %i (%p)\n", i, retaddr);
break;
}
}
}
They seem to use this generic algorithm for detecting a range of gadgets. Further analysis is to be done on it.
// this code can be inlined and also be in their own sub
if ( v14 == -1 )
{
pCurrentStackTraceRIP = pStackTrace;
nStackTraceIndex = 0;
do
{
retaddr = *(_QWORD *)pCurrentStackTraceRIP;
if ( !*(_QWORD *)pCurrentStackTraceRIP || (index = 0, r5::ac::max_whitelisted_sections <= 0) )
{
on_detected_violation2:
r5::ac::push_violation((__int64)aCsCinputInputC, 1);
goto LABEL_17;
}
whitelisted_sections = (unsigned __int64 *)r5::ac::whitelisted_sections;
// v17 looks like a RETURN ADDRESS
// - at index zero, pcurr[0] is start of text section and pcurr[1] is end of .text section
while ( retaddr < *whitelisted_sections || retaddr > whitelisted_sections[1] )
{
++index;
whitelisted_sections += 2;
if ( index >= r5::ac::max_whitelisted_sections )
goto on_detected_violation2;
}
if ( *(_BYTE *)(retaddr - 5) != 0xE8 )
{
some_idx = 2i64;
while ( *(_BYTE *)(retaddr - some_idx) != 0xFF
|| (((*(_BYTE *)(retaddr - some_idx + 1) & 0x38) - 16) & 0xF7) != 0 )
{
if ( ++some_idx > 6 )
goto on_detected_violation2;
}
}
++nStackTraceIndex;
pCurrentStackTraceRIP = (__int128 *)((char *)pCurrentStackTraceRIP + 8);
}
while ( nStackTraceIndex < numStackTraceRecords2 );
}
}
}