I've been examining the PE file structure graph located here. The following is written there about the export directory:

If a symbol N is exported by ordinal and name then:
-Its name will be located at AddressOfNames[N]

-Its ordinal at AddressOfNameOrdinals[N]

-And its address* will be AddressOfFunctions[AddressOfNameOrdinals[N]]

I thought that the address would be AddressOfFunctions[AddressOfNameOrdinals[N] - _IMAGE_EXPORT_DIRECTORY.Base] because Base is the number that we subtract from the ordinal to get the index into the AddressOfFunctions array.

For example, if we have Base = 8, the minimal entry in AddressOfNameOrdinals will be 8. The first index of the AddressOfFunctions array is zero, so to get the address of the function with the ordinal equal to 8, we subtract the base from the ordinal and get 0.

Where have I made a mistake?

3 Answers 3


If you don't have the GetProcAddress() function, you have to iterate entire export table, here a example how i made it in the past...

template<typename INTH_T>
NTSTATUS GetRemoteProcAddressArch(HANDLE hProcess, HMODULE ModuleHandle, LPCSTR RoutineName, PULONG_PTR hRoutine)

    HMODULE hModule = GetModuleHandleA("Ntdll.dll");

        hModule = LoadLibraryA("Ntdll.dll");


            HANDLE ProcessHandle,
            PVOID BaseAddress,
            PVOID Buffer,
            SIZE_T NumberOfBytesToRead,
            PSIZE_T NumberOfBytesReaded
        ) = reinterpret_cast<NTSTATUS(WINAPI*)(HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T)>(GetProcAddress(hModule, "ZwReadVirtualMemory"));

            const size_t rLength = strlen(RoutineName);
            char* rNameSz = new char[rLength + 1] { 0 };

                PIMAGE_DOS_HEADER iDos = reinterpret_cast<PIMAGE_DOS_HEADER>(ModuleHandle);
                SIZE_T BytesReaded = NULL;
                LONG PEHeaderVirtualAddress = NULL; // same both x86 and x64, shuldn't be problem...
                NTSTATUS rResult = ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(&iDos->e_lfanew), &PEHeaderVirtualAddress, sizeof(LONG), &BytesReaded);

                if(rResult == STATUS_SUCCESS)
                    INTH_T* PEHeader = reinterpret_cast<INTH_T*>(PEHeaderVirtualAddress + reinterpret_cast<ULONG_PTR>(ModuleHandle));

                    ULONG ImageExportDirectoryVirtualAddress = NULL; // same both x86 and x64
                    BytesReaded = NULL;
                    rResult = ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(&PEHeader->OptionalHeader.DataDirectory[0]), &ImageExportDirectoryVirtualAddress, sizeof(ULONG), &BytesReaded);

                    if(rResult == STATUS_SUCCESS)
                        PIMAGE_EXPORT_DIRECTORY nED = reinterpret_cast<PIMAGE_EXPORT_DIRECTORY>(reinterpret_cast<ULONG_PTR>(ModuleHandle) + ImageExportDirectoryVirtualAddress);
                        IMAGE_EXPORT_DIRECTORY ed = { 0 };

                        BytesReaded = NULL;
                        if(ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(nED), &ed, sizeof(IMAGE_EXPORT_DIRECTORY), &BytesReaded) == STATUS_SUCCESS)

                            PDWORD rvaNames = reinterpret_cast<PDWORD>(reinterpret_cast<ULONG_PTR>(ModuleHandle) + static_cast<ULONG_PTR>(ed.AddressOfNames));
                            PDWORD rvaFunctions = reinterpret_cast<PDWORD>(reinterpret_cast<ULONG_PTR>(ModuleHandle) + static_cast<ULONG_PTR>(ed.AddressOfFunctions));
                            PWORD rvaOrdinals = reinterpret_cast<PWORD>(reinterpret_cast<ULONG_PTR>(ModuleHandle) + static_cast<ULONG_PTR>(ed.AddressOfNameOrdinals)); 

                            for(DWORD i = 0; i < ed.NumberOfFunctions; i++)
                                BytesReaded = NULL;
                                DWORD rva = NULL;
                                if(ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(&rvaNames[i]), &rva, sizeof(DWORD), &BytesReaded) == STATUS_SUCCESS)
                                    BytesReaded = NULL;
                                    if(ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(reinterpret_cast<ULONG_PTR>(ModuleHandle) + static_cast<ULONG_PTR>(rva)), reinterpret_cast<PVOID>(const_cast<LPSTR>(rNameSz)), rLength, &BytesReaded) == STATUS_SUCCESS)
                                        if(!ncstrncmp(rNameSz, RoutineName, rLength))
                                            BytesReaded = NULL;
                                            WORD rvaOrdinal = NULL;

                                            if(ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(&rvaOrdinals[i]), &rvaOrdinal, sizeof(WORD), &BytesReaded) == STATUS_SUCCESS)
                                                BytesReaded = NULL;
                                                rva = NULL;

                                                if(ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(&rvaFunctions[rvaOrdinal]), &rva, sizeof(DWORD), &BytesReaded) == STATUS_SUCCESS)
                                                    *hRoutine = (static_cast<ULONG_PTR>(rva) + reinterpret_cast<ULONG_PTR>(ModuleHandle));
                                                    Result = STATUS_SUCCESS;            
            delete[] rNameSz;
    return Result;

usage example:

auto hProcess = OpenProcess( ... );
auto possibleHandle = RemoteHandleFrom(hProcess, "ntdll.dll"); // i owe you, but you can find it in the msdn, there's a lot examples...
GetRemoteProcAddressArch<IMAGE_NT_HEADERS64>(hProccess, possibleHandle, "ZwOpenProcess", &addr );
  • Apologies for a late question. In your code you are writing: if (ZwReadVirtualMemory(hProcess, reinterpret_cast<PVOID>(&rvaFunctions[rvaOrdinal]), &rva, sizeof(DWORD), &BytesReaded) == STATUS_SUCCESS). But why isn't it &rvaFunctions[rvaOrdinal-Base]? Shouldn't we subtract it from the ordinal because the array members start at 0 and the ordinals start at the number written in the Base field?
    – Student12
    Commented Jan 11, 2020 at 20:53
  • @Student12 No, we add the relative virtual address to our current module base, since the function assembly it's on (OurCurrentModuleBase + rva), when a module it's mapped into memory, it's mapped with forward memory address logic... like first we have the Dos Stub, PE header, sections .text, .data, etc, so we have to add it... since our base address it's before than all...
    – MindLerp
    Commented Jan 15, 2020 at 18:41
  • What strikes me as odd about this function is that your for loop uses NumberOfFunctions as the upper boundary. This seems wrong to me, as the NumberOfNames governs the size of the parallel AddressOfNameOrdinals and AddressOfNames arrays. What am I missing? Won't you overrun with this code?
    – 0xC0000022L
    Commented Feb 2, 2022 at 22:18

The ordinal base is the value that you have to add to the ordinal to get the actual/true ordinal used for importing.

The example code below should explain what I mean:

//p_module_base is a char* pointer to the module base 
//p_export_directory is a PIMAGE_EXPORT_DIRECTORY pointer for the module
PDWORD p_func_names = (PDWORD)(p_module_base + p_export_directory->AddressofNames);
PWORD p_ordinals = (PWORD)(p_module_base + p_export_directory->AddressOfNameOrdinals);

//As an example, consider the 0th named function in the DLL:
char * example_name = (char*)(p_module_base + p_func_names[0]);
WORD example_ordinal = p_ordinals[0];

//Adjust the ordinal by adding the ordinal base
WORD true_ordinal = example_ordinal + p_export_directory->Base;

//The following two returned addresses are THE SAME:
FARPROC addr_one = GetProcAddress((HMODULE)p_module_base, (LPCSTR)example_name);
FARPROC addr_two = GetProcAddress((HMODULE)p_module_base, (LPCSTR)true_ordinal);

UPDATE (to address the comment):

To get the function address without GetProcAddress, you index into the Function address table and then add the module base. The addresses are RVAs.

Example code:

//List of function RVAs
PDWORD p_addresses = (PDWORD)(p_module_base + p_export_directory->AddressOfFunctions);

//For our example function considered in the code above, get the address
PVOID example_address = (PVOID)(p_module_base + p_addresses[p_ordinals[0]]);

printf("The function named %s is located in memory here: %p\n", 
  • What if I don't have the GetProcAddress function and have to search for the exports manually? I parse the export directory, find the name of the function in the names array, then I find the ordinal in the ordinals array. What index do I use now to get the function address from the EAT?
    – Student12
    Commented Jan 6, 2020 at 9:38
  • 1
    @Student12 please see my update to address your comment
    – hft
    Commented Jan 8, 2020 at 2:47

From what I can see by viewing Kernelbase.dll in PE Bear, the First Function AccessCheck() is at AddressOfNames[0], and the corresponding Ordinal at AddressOfOrdinal[0] is 4, which according to the PDF would mean the Address of the Function would be at AddressOfFunctions[4], which it is. Which means there are 4 functions defined in the AddressOfFunctions Table before AccessCheck(), at positions AddressOfFunctions[0] to AddressOfFunctions[3]. Although there is _IMAGE_EXPORT_DIRECTORY.Base value of 1 in the dll, I'm not sure how that is coming into play as the PDF description appears to be correct. I will have to see how the function names of the first 4 functions before AccessCheck are mapped with the first 4 addresses in the AddressOfFunctions table though.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.