If you've worked with classic Windows COM for any decent amount of time, you've almost certainly run into this frustrating reality: an interface pointer is not a universal passport. It works perfectly in the thread (apartment) where it was obtained, but hand it off to another thread and things quickly go south; crashes, deadlocks, and other mysterious failures.
This is exactly the problem the Global Interface Table (GIT) was created to solve.
What we're dealing with
The official interface is IID_IGlobalInterfaceTable = {00000146-0000-0000-C000-000000000046}, and its implementation sits behind CLSID_StdGlobalInterfaceTable = {00000323-0000-0000-C000-000000000046}. Both GUIDs belong to the classic OLE family — you can tell by the C000...46 ending. In reverse engineering, this is a reliable breadcrumb: find the IID, locate the vtable, and you've usually found the implementation.
Modern combase.dll is the merged runtime for both COM and WinRT. Starting with Windows 8, Microsoft moved the core COM machinery out of ole32.dll into combase.dll and brought WinRT activation along with it. Both technologies now share the same apartment model.
You can see this connection indirectly: calling time zone functions on Windows 10 pulls in combase.dll and triggers WinRT initialization. That's why RoInitialize appears as a relatively "expensive" function, it's implemented there. In fact, it's a superset of CoInitialize. On threads using both COM and WinRT, calling RoInitialize is the safer and more correct choice.
The GIT itself is a true process-wide singleton. It isn't created lazily on the first CoCreateInstance call. Instead, it's initialized early, during combase.dll loading, in the CRT dynamic initializers phase and destroyed on unload via atexit.
This explains why CoCreateInstance(CLSID_StdGlobalInterfaceTable, ...)doesn't actually create a new object: the class factory simply returns the already-existing _git instance.
Internally, the public RegisterInterfaceInGlobal is just a thin wrapper that calls the helper RegisterInterfaceInGlobalHlp, passing an extra ulong parameter. This is a classic pattern: the public method is a shim that hardcodes a flag for the internal implementation. That flag likely controls the registration mode. There's a strong suspicion that the same helper also powers WinRT agile references (RoGetAgileReference / IAgileReference). In other words, one internal infrastructure serves both classic GIT and modern agile mechanisms.
The GIT works elegantly: you register an interface pointer and receive a compact cookie, essentially just an integer. Later, even from a different thread or apartment, you hand that cookie back and get a fully usable interface pointer in return. COM handles all the hard parts for you: marshaling, proxies, apartment transitions, and lifetime management.
The public API is straightforward:
RegisterInterfaceInGlobal - registers the object and returns a cookie
GetInterfaceFromGlobal - retrieves a usable pointer
RevokeInterfaceFromGlobal - removes the entry
This is especially useful in applications with a UI thread (STA) and a pool of worker threads (MTA) that need to safely share COM objects.
Why hunt for GIT in memory?
Mainly for research, debugging stubborn COM objects, building your own tools, and gaining a deeper understanding of how Windows really works.
Importantly, the GIT lives inside combase.dll of the target process (unlike the Running Object Table, which resides in rpcss).
Here are two practical ways to locate it without symbols.
Method 1: Find the global gGIPTbl object
The most direct approach is to locate the global CGIPTable object, often referred to as gGIPTbl.
In x64 code, it's typically accessed via RIP-relative addressing:
lea rcx, [rip + 0x12345678]
Calculate the actual address, and you have a pointer to the table. From there, you just need to reverse-engineer enough of its layout to reach the entries.
Method 2: Follow the allocator footprints
I prefer this method. Instead of hunting the table directly, find the code that validates cookies during resolution.
When COM decodes a cookie, it extracts the page index, entry index, and sequence number, calculates the entry address within the page-based allocator, and performs a quick validation.
In one build, the characteristic "validation trio" looked like this:
cmp dword ptr [rdi+0x1C], ebp ; sequence
cmp dword ptr [rdi+0x18], 0 ; type / in use
mov eax, dword ptr [rdi+0x20] ; usage count
Even if offsets change over time, the overall pattern remains recognizable. Once you find the function, walk backwards to locate the RIP-relative loads for the allocator globals: start of the page list, entry size, entries per page, and so on.
Read those addresses from process memory, and you can walk the pages to extract active GIT entries.
Building a lab tool
The pipeline is fairly clean:
Open the target process with PROCESS_VM_READ access.
Locate the base address of combase.dll.
Read and disassemble its .text section (e.g. with Capstone).
Search for the validation pattern.
Extract the allocator global addresses.
Walk the pages and dump active entries.
No PDBs required, just pattern matching and careful memory reading. Naturally, this approach is fragile: Windows updates can break your patterns. But that's part of the fun, when one pattern stops working, you go hunting for the next.
The GIT exists because interface pointers can't magically teleport between apartments. It's the official cloakroom system: you check in your interface, get a ticket (cookie), and later retrieve a working version from any context.
You can reach it by tracking the global object or by following the allocator footprints. Both approaches work. Choose the one that fits your style and tolerance for version changes.
Top comments (0)