Skip to main content

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.