Table of Contents


How it Works

In DirectX 12, the ID3D12Device::CreateDescriptorHeap function is responsible for creating descriptor heaps, which store views of resources like textures, constant buffers, and samplers. Every modern DirectX 12 application uses this function to allocate GPU descriptors.

ImGui's Use of CreateDescriptorHeap

ImGui's official DirectX 12 backend calls CreateDescriptorHeap during initialization to build shader resource view (SRV) descriptor heaps for its font atlas and any other textures it needs to render UI. This means that whenever an overlay or injected ImGui menu is loaded into a game, CreateDescriptorHeap will inevitably be called. For anticheat developers, this creates a predictable detection vector: monitor the integrity of the CreateDescriptorHeap signature, and you can identify whether a third-party library like ImGui has hooked it.

Inline Hooking of CreateDescriptorHeap

An inline hook works by overwriting the function's first few bytes with a relative jmp instruction that transfers control to custom code. In d3d12core.dll, the signature of CreateDescriptorHeap typically begins with the following bytes:

                
  0x40, 0x53, 0x56, 0x57, 0x41, 0x56
            
Which disassemble to:
                          
  40 53 push rbx
  56 push rsi
  57 push rdi
  41 56 push r14
                      

These instructions save callee saved registers, a normal part of the function's setup. When an inline hook is applied, these bytes are replaced with a jump instruction. For example:

                                    
  E9 ?? ?? ?? ?? jmp <cheat_hook>
  90 nop
                  

The E9 opcode is a relative jump in x86-64, followed by a 4-byte offset to the hook's destination address. A nop is sometimes added for padding if the overwritten instruction block is longer than 5 bytes.

Before (original signature):
                                              
  CreateDescriptorHeap+0x0:
  40 53 push rbx
  56 push rsi
  57 push rdi
  41 56 push r14
                   
After (inline hook applied):
                                                                  
  CreateDescriptorHeap+0x0:
  E9 12 34 56 78 jmp 0x78563412 ; redirect to cheat hook
  90 nop ; padding
                                       

With this in place, every call to CreateDescriptorHeap first executes the malicious jump, transferring execution to the cheat's custom handler.

That handler can:

For anticheat systems, CreateDescriptorHeap serves as a reliable detection vector: by hooking this function, the anticheat can monitor when overlays like ImGui initialize, since they must call it to create their descriptor heaps. This makes it a lightweight but effective way to catch injected menus, especially when paired with call-origin checks.

The Bypass

Aware of this detection vector, a cheat developer could attempt to restore the original signiture bytes after their own code has been injected. By doing so, the inline jmp is overwritten with the expected sequence (0x40 0x53 0x56 0x57 0x41 0x56) effectively “unhooking” the function in memory while still leaving their injected cheat active. The code snippet below shows a simplified version of this idea:

                    
  namespace rendering::dx
  {
    ID3D12Device* device = nullptr;

    bool create(IDXGISwapChain3* chain)
    {
      if (!chain)
        return false;
      
      if (chain->GetDevice(__uuidof(device), reinterpret_cast<void**>(&device)) != S_OK)
        return false;

      if (auto d3d12 = reinterpret_cast<uintptr_t>(utils::memory::get_module_base("D3D12Core.dll")))
      {
        // Restore original CreateDescriptorHeap signature
        utils::memory::patch(d3d12 + 0x88A10, { 0x40, 0x53, 0x56, 0x57, 0x41, 0x56 });
      }

      // Custom function that Sets up render targets for each swap chain frame.
      create_render_target(chain);
      return true;
    }
  }
                    

Fixing the Bypass

To counter this bypass, anticheats can extend their checks beyond static byte comparison. Possible strategies include:

In short: simply restoring bytes is not enough if the anticheat validates the flow of execution rather than just the static function signature.

Summary

The CreateDescriptorHeap function is a fundamental part of DirectX 12, responsible for allocating descriptor heaps that games and overlays like ImGui rely on. Because ImGui's DirectX 12 backend calls this function during initialization, it exposes a natural detection point for anticheat systems: if the function is hooked, it's often a strong signal that an injected cheat menu is present.

Cheat developers, however, could figure out this detection vector and may attempt to bypass it by restoring the original signature bytes after injection, while moving their hooks to other rendering functions such as Present or ResizeBuffers. From a surface-level integrity check, CreateDescriptorHeap appears unmodified, but the cheat still operates in the background.

For anticheat systems, the fix is to go beyond static byte checks. By validating call stacks, verifying IAT and VTable integrity, and comparing in-memory function bodies with trusted disk images, anticheats can detect even sophisticated bypass attempts. Monitoring the actual flow of execution rather than just the function's entry bytes ensures that cheats cannot rely solely on “restoring” instructions to stay hidden.

In essence, the CreateDescriptorHeap hook represents a classic example of the ongoing arms race between cheats and anticheats. Each side continuously adapts—cheats seeking stealthier injection methods, and anticheats developing deeper validation strategies. Understanding this interaction highlights both the technical elegance of ImGui's rendering integration and the challenges developers face in safeguarding modern multiplayer games.