Hey everyone. In this article, we’ll break down how to work with physical memory and why using virtual memory read/write in the context of popular anti-cheats is a bad approach.

Basic Terms

  • CR3 - Control Register 3
  • DTB - Directory Table Base
  • PML4 - Page Map Level 4
  • PDPT - Page Directory Pointer Table
  • PD - Page Directory
  • PT - Page Table

Two Ways to Read Memory

  1. Virtual memory reading ( MmCopyVirtualMemory )
  2. Physical memory reading ( MmMapIoSpace, MmCopyMemory with MM_COPY_MEMORY_PHYSICAL flag )

Why P2C’s dont use virtual memory reading

When you call MmCopyVirtualMemory, it eventually calls KiStackAttachProcess which attaches to the target process. During this attach:

  • The target process DTB gets written to CR3
  • The target process gets written to ApcState.Process of the current thread

Anti-cheats easily detect this by checking threads’ ApcState. If ApcState.Process equals the game process or CR3 contains the game’s DTB -> instant ban or report.

Translating Virtual to Physical Address

For example: to translate 0xdeadc0dedeadbeef, we walk the page table chain: PML4E -> PDPT -> PD -> PT -> PA We need the target process DTB which is unique per process. Without it, physical memory is useless ( For us ).

Translation Code

uint64_t translate_linear_address( uint64_t directory_table_base, uint64_t address ) {
    uint16_t pml4 = ( uint16_t )( ( address >> 39 ) & 0x1FF );
    uint16_t pdpt = ( uint16_t )( ( address >> 30 ) & 0x1FF );
    uint16_t pd   = ( uint16_t )( ( address >> 21 ) & 0x1FF );
    uint16_t pt   = ( uint16_t )( ( address >> 12 ) & 0x1FF );

    uint64_t pml4e{ };
    read( directory_table_base + ( uint64_t )pml4 * sizeof( uint64_t ), ( uint8_t* )&pml4e, sizeof( pml4e ) );

    if ( !pml4e ) 
        return 0;

    uint64_t pdpte{ };
    read( ( pml4e & 0xFFFF1FFFFFF000 ) + ( uint64_t )pdpt * sizeof( uint64_t ), ( uint8_t* )&pdpte, sizeof( pdpte ) );

    if ( !pdpte )
        return 0;

    // 1GB page
    if ( ( pdpte & ( 1 << 7 ) ) != 0 )
        return ( pdpte & 0xFFFFFC0000000 ) + ( address & 0x3FFFFFFF );

    uint64_t pde{ };
    read( ( pdpte & 0xFFFFFFFFFF000 ) + ( uint64_t )pd * sizeof( uint64_t ), ( uint8_t* )&pde, sizeof( pde ) );

    if ( !pde ) 
        return 0;

    // 2MB page
    if ( ( pde & ( 1 << 7 ) ) != 0 )
        return ( pde & 0xFFFFFFFE00000 ) + ( address & 0x1FFFFF );

    uint64_t pte{ };
    read( ( pde & 0xFFFFFFFFFF000 ) + ( uint64_t )pt * sizeof( uint64_t ), ( uint8_t* )&pte, sizeof( pte ) );

    if ( !pte )  
        return 0;

    return ( pte & 0xFFFFFFFFFF000 ) + ( address & 0xFFF );
} 

Writing to Physical Memory

  1. Take the target virtual address
  2. Using the process DTB, call translate_linear_address to get the physical address
  3. Call MmMapIoSpace to map the physical memory ( can write even without PTE.Writable flag )
  4. Copy data via memcpy
  5. Free the mapped section with MmUnmapIoSpace

Reading Physical Memory

Call MmCopyMemory with MM_COPY_MEMORY_PHYSICAL flag.

NTSTATUS read_phys_address( void* address, void* buffer, size_t size, size_t* read ) {
    MM_COPY_ADDRESS copy{ };
    copy.PhysicalAddress.QuadPart = LONGLONG( address );

    return MmCopyMemory( buffer, copy, size, MM_COPY_MEMORY_PHYSICAL, read );
}

Finding DTB

  • If the game does NOT encrypt DTB: read DirectoryTableBase from KPROCESS.
  • If DirectoryTableBase is zero: try UserDirectoryTableBase.
uint32_t get_user_directory_table_base_offset( ) {
    RTL_OSVERSIONINFOW ver{ };
    RtlGetVersion( &ver );

    if ( ver.dwBuildNumber <= 20180 )
        return 0x0388;
    else if ( ver.dwBuildNumber <= 19041 )
        return 0x0280;
    else if ( ver.dwBuildNumber >= 26100 )
        return 0x0158;
    else
        return 0x0278;

    return 0;
}

uint64_t get_process_dtb( PEPROCESS process ) {
    const auto offset = get_user_directory_table_base_offset( );
    const auto dir_base = *reinterpret_cast< uint64_t* >( ( uint8_t* )process + 0x28 );

    if ( !dir_base )
        return *reinterpret_cast< uint64_t* >( ( uint8_t* )process + offset );

    return dir_base;
}

If the Game Encrypts DTB ( EAC on Fortnite, Rust, etc )

How EAC does it:

  1. It places hook on KdTrap ( callback that called in KiDispatchException )
  2. Writes invalid value to KPROCESS + 0x28 ( DirectoryTableBase )
  3. When encrypted register is used, CPU generates exception which calls KiDispatchException. Hooked KdTrap handles and fix that

Method 1: Attach to Process + Read CR3

uint64_t get_process_cr3( PEPROCESS process ) {
    KAPC_STATE state{ };
    KeStackAttachProcess( process, &state );

    const auto cr3 = __readcr3( );

    KeUnstackDetachProcess( &state );

    return cr3;
}
uint64_t get_process_cr3( uint64_t game_base_address ) {
    for ( uint64_t candidate{ }; candidate < 0x400000000; candidate += 0x1000 ) {
        const auto phys = translate( candidate, game_base_address )

        if ( !phys )
            continue;

        const auto pool = winapi::allocate_pool( 0x1000 );

        if ( !read_buffer( phys, pool ) )
            continue;

        const auto dos_header = reinterpret_cast< IMAGE_DOS_HEADER* >( pool );

        if ( dos_header->magic != 0x5A4D )
            continue;

        const auto nt_headers = reinterpret_cast< IMAGE_NT_HEADERS* >( reinterpret_cast< uint64_t >( pool ) + dos_header->e_lfanew );

        if ( nt_headers->Signature != 0x4550 )
            continue;

        if ( nt_headers->OptionalHeader.ImageBase == game_base_address ) {
            winapi::free_pool( pool );
            return candidate;
        }
    }
 
    return { };
}

Why bruteforce is better

  1. Safest - no need to change thread context
  2. Fast - if you limit the search range
  3. Accurate - anti-cheat can’t fake physical memory

Important: You may need to refresh CR3 periodically, as the anti-cheat could reallocate tables and invalidate the old DTB.

Conclusion

Virtual memory reading is a poor method because it leaves many traces and is easily detected by anti-cheats.

If you are building P2C:

  • Always use physical memory reading
  • If you encounter encrypted DTB, use bruteforce to obtain it

Useful Resources