Hey everyone. In this article, we’ll break down some of the protection mechanisms used in the EasyAntiCheat_EOS driver (Rust-based EAC was used, build date around March 10th of this year).

Based on reverse engineering results, we’ll cover:

  1. Telemetry collection
  2. System driver integrity checks
  3. Report encryption

Telemetry collection

EAC collects information about every running process and sends it to the server. It uses LoadImageNotify as a callback for this.

The anti-cheat collects process telemetry to:

  • Identify potentially dangerous processes (known cheats, injectors, etc.)
  • Build behavior patterns across different users
  • If the game crashes — help determine which process might have caused it

Pseudocode:

void collect_process_telemetry( PEPROCESS process, bool use_extended_info ) {
    const auto packet = allocate_pool( 0x330 );

    if ( memory::is_valid( packet ) ) {
        // Get process information

        const auto is_wow64 = PsGetProcessWow64Process( process ) != 0;
        const auto pid = PsGetProcessId( process );

        const auto process_info = copy_process_info( 0, 0, 0, 0x801, is_wow64, 0, pid, 0 );

        // If extended info flag is set, copy the process name into the data buffer

        if ( memory::is_valid( process_info ) ) {
            if ( use_extended_info ) {
                char buffer[ 16 ]{ };

                // PsGetProcessImageFileName

                if ( get_process_name( process, buffer ) ) {
                    auto dest = process_info + 0x20;

                    for ( int idx{ }; idx < 15 && buffer[ idx ]; ++idx )
                        dest[ idx ] = ( wchar_t )buffer[ idx ];

                    dest[ 15 ] = 0;
                }
            }
        }

        // Fill packet header

        *( uint64_t* )packet = process_info; // Pointer to data buffer
        *( DWORD* )( packet + 0x28 ) = 1; // Status
        *( DWORD* )( packet + 0x30 ) = 0x7429AC76; // Packet type
        *( DWORD* )( packet + 0x40 ) = 0; // Flags (?)
        *( uint64_t* )( packet + 0x38 ) = 0; // Additional information

        // Send packet to server

        send_telemetry_packet( packet, 0 );
        ex_free_pool_with_tag( process_info );
    }
}
  1. Copy process information ( parse PE headers into required format, copy process name if extended info flag is set )
  2. Allocate pool, fill with data
  3. Send to server

System driver integrity checks

EAC iterates through every loaded driver ( retrieved via ZwQuerySystemInformation with SystemModuleInformation flag ), obtains their DriverObject, and checks each MajorFunction, verifying the dispatch address. The anti-cheat does this to detect hooks in system drivers. Hooks are commonly used for spoofers or usermode communication.

bool check_driver_dispatch( PDRIVER_OBJECT driver_object, uint32_t index, void* buffer, uint32_t* status_code ) {
    if ( status_code )
        *status_code = 0;

    if ( !driver_object ) {
        if ( status_code )
            *status_code = 2; // driver_object not found

        return false;
    }

    const auto major_function = driver_object->MajorFunction[ index ];

    if ( !major_function ) {
        if ( status_code )
            *status_code = 4; // No implementation for this index

        return false;
    }

    const auto base_address = driver_object->DriverStart;
    const auto size = driver_object->DriverSize;

    if ( !base_address
         || !size ) {
        if ( status_code )
            *status_code = 5; // Failed to determine driver bounds

        return false;
    }

    bool found{ };

    if ( major_function >= base_address && major_function <= ( base_address + size ) ) {
        if ( status_code )
            *status_code = 7; // Check passed, function not hooked

        found = true;

        if ( buffer ) {
            *( uint32_t* )buffer = major_function - base_address; // Save function RVA
            *( uint32_t* )( ( uintptr_t )buffer + 4 ) = size; // Save driver size

            memcpy( ( char* )buffer + 8, get_module_name( base_address ), 255 ); // Copy module name
        }
    } else
        if ( status_code )
            *status_code = 6; // Function points outside module => DETECTED

    return found;
}
  1. Check if function resides within the module
  2. Copy driver information
  3. Send report if status_code != 7 or !found ( report sending is in the function that calls the dispatch check )

Report encryption

EAC generates an encrypted key using values from KUSER_SHARED_DATA. The anti-cheat does this to hide the real information sent to the server, preventing simple emulation.

void create_encrypted_key( ) {
    const auto buffer = reinterpret_cast< uint8_t* >( allocate_pool( 0x1000 ) );

    if ( !buffer )
        return;

    // 0x2C4 -> SystemExpirationDate
    // 0x14 -> SystemTime
    // 0x260 -> BootId

    const auto v16 = *( ULONG* ) 0xFFFFF780000002C4 ^ *( ULONG* )0xFFFFF78000000014;
    const auto v17 = *( ULONG* )0xFFFFF78000000260 ^ *( ULONG* )0xFFFFF78000000008;
    const auto v18 = ( 1533831705 * ( buffer >> 2 ) ) ^ *( LONG* )0xFFFFF78000000018;
    const auto v19 = *( LONG* )0xFFFFF7800000000C ^ ( 1533831705 * ( ( unsigned __int64 )&buffer >> 2 ) );

    buffer[ 4 ] = v18 ^ 0x97B9BC33;
    *buffer = -96604097;
    buffer[ 1 ] = -345804981;
    buffer[ 5 ] = v17 ^ 0xE220A00D;
    buffer[ 2 ] = 144253742;
    buffer[ 7 ] = 897887310;
    buffer[ 3 ] = v16 ^ 0xA868AC37;
    buffer[ 8 ] = -123907290;
    buffer[ 6 ] = v19 ^ 0x79BB2376;

    /* 
      v15 = buffer;
      v20 = v19 ^ v16 ^ v18 ^ v17 ^ 0x4C53A039;
    */
}
  1. Generates an encrypted key for further encryption of the report buffer.

Conclusion

With this article, you can learn useful information about the inner workings of EasyAntiCheat. I hope this article was helpful!