Během programování (hlavně shellcodů) jsem byl často konfrontován s potřebou disponovat vlastní funkcí GetProcAddress[1]. Tato funkce se používá pro získávání adresy konkrétní exportované funkce z konkrétní DLL knihovny. Tento krátký článek ukáže zájemcům, jak si vytvořit svoji vlastní verzi této funkce, případně předvede, jak tato funkce funguje.

Obsah:
——————————————
1. Obecné seznámení
2. Realizace kódu
3. Samotný kód
4. Odkazy
——————————————

1. Obecné seznámení
Pokud se podíváme do dokumentace, uvidíme následující prototyp funkce:

FARPROC WINAPI GetProcAddress(
  __in  HMODULE hModule,
  __in  LPCSTR lpProcName
);

Prvním argumentem je handle daného modulu (DLL knihovny). Tento handle vrací funkce LoadLibrary[2], LoadLibraryEx[3] nebo GetModuleHandle[4]. Druhý argument udává jméno funkce, jejíž adresu v paměti chceme získat. Příklad použití může vypadat třeba takhle:

GetProcAddress(LoadLibrary("kernel32.dll"), "GetProcAddress");

Výše zmíněný kód vrátí adresu funkce GetProcAddress v DLL knihovně kernel32.dll.

2. Realizace kódu
Samotný kód funkce můžeme rozdělit na dvě základní části. První část získá adresu požadované funkce a druhá část zajistí případný forwarding (bude vysvětleno níže). První argument hModule nám poskytuje adresu, na které v paměti začíná daná DLL knihovna. Protože tyto knihovny jsou vlastně PE soubory, můžeme s nimi i takovým způsobem pracovat. (Cílem článku není podrobně vysvětlit strukturu PE souboru, a proto budou zmíněny pouze důležité věci s předpokladem, že si čtenář, v případě potřeby, zbylé informace nastuduje sám).

Každý PE soubor začíná strukturou IMAGE_DOS_HEADER[5], ze které nás zajímají dvě pole: e_magic a e_lfanew. První obsahuje DOS signaturou a má hodnotu ‚MZ‘, druhé popisuje RVA místa, kde se nachází offset začátku PE hlavičky. PE hlavičku tvoří strukturA IMAGE_NT_HEADERS[6], která obsahuje tři pole, z nichž nás zajímají pouze dvě: Signature a OptionalHeader, tedy struktura IMAGE_OPTIONAL_HEADER[7]. První označuje signaturu PE hlavičky (jak název sám napovídá) a má hodnotu „PE\0\0“, druhá popisuje souhrnné informace o daném obrazu PE souboru. Ve struktuře IMAGE_OPTIONAL_HEADER si vystačíme s polem DataDirectory, tedy pole struktur IMAGE_DATA_DIRECTORY[8]. Z tohoto pole struktur si vytáhneme první člen, který obsahuje adresu a velkost tabulky exportovaných funkcí (tvořena strukturou IMAGE_EXPORT_DIRECTORY[9]).

Teď přijde to hlavní: Tato struktura obsahuje pole AddressOfNames. To je pointer na pole pointerů, které odkazují na řetězce všech pojmenovaných exportovaných funkcí, které se v dané knihovně nachází. Cyklicky tedy projdeme celé pole a počítáme iterace. Ve chvíli, kdy najdeme jméno požadované funkce, vynásobíme iterátor velikostí datového typu WORD. Tuto hodnotu přičteme k AddressOfNameOrdinal v IMAGE_EXPORT_DIRECTORY (jedná se o pointer na pole pointerů které obsahuje ordinální hodnoty jednotlivých funkcí).

!!!UPOZORNĚNÍ!!!
Ordinální hodnota funkce se nemusí vždy rovnat pozici řetězce definujícího jméno funkce!!

Nyní, když máme ordinální hodnotu funkce, můžeme získat její adresu. Vynásobíme tuto ordinální hodnotu velikostí datového typu DWORD, čímž získáme RVA požadované funkce. Zde se na scénu dostává druhá část – forwarding.

Tak, jako aplikace používají exportované funkce DLL knihoven, tak i samotné knihovny mohou využívat exportované funkce jiných DLL knihoven (a vlastně každá aplikace využívá minimálně dvě: kernel32.dll a ntdll.dll, přičemž samotná kernel32.dll využívá funkcí z ntdll.dll). Například funkce EnterCriticalSection z kernel32.dll se forwarduje k funkci RtlEnterCriticalSection z ntdll.dll. Logicky, samotnou funkci RtlEnterCriticalSection v adresním prostoru kernel32.dll prostě nenajdeme. Ale jak poznáme, zda je daná funkce forwardována či nikoliv? Jednoduše. Pokud totiž její RVA ukazuje do sekce exportů (určené hodnotami VirtualAddress a Size ve struktuře IMAGE_DATA_DIRECTORY). Pak se jedná o pointer na řetězec ve formátu knihovna.funkce, např. NTDLL.RtlEnterCriticalSection. Patrně již víte, co musíme udělat. Jednoduše získat handle modulu dané knihovny a opět v ní najít danou adresu funkce.

3. Samotný kód
Na závěr přikládám celý kód, včetně ukázkového příkladu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <windows.h>
#include <stdio.h>
 
FARPROC WINAPI MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName){
  #define hMod (DWORD)hModule
  PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER) hModule;
  PIMAGE_NT_HEADERS pNtHdrs;
  PIMAGE_EXPORT_DIRECTORY pExportDir;
  DWORD ExportTableAddr, ExportTableEnd, i = 0, *pAddress;
  DWORD *pFunctionNameVA;
  WORD *pOrdinal;
  char szDll[MAX_PATH], szFunc[MAX_PATH], *pDllName;
 
  memset(szDll, 0, MAX_PATH);
  memset(szFunc, 0, MAX_PATH);
 
  if(hModule == NULL)
    return NULL;
 
  if(pDosHdr->e_magic != 0x5A4D) // ZM
    return NULL;
 
  pNtHdrs = (PIMAGE_NT_HEADERS)(hMod + 
                                pDosHdr->e_lfanew);
 
  if(pNtHdrs->Signature != 0x00004550) // \0\0EP
    return NULL;
 
  ExportTableAddr = pNtHdrs->OptionalHeader.DataDirectory[0].VirtualAddress;
  ExportTableEnd = ExportTableAddr +
                   pNtHdrs->OptionalHeader.DataDirectory[0].Size;
  pExportDir = (PIMAGE_EXPORT_DIRECTORY)(pNtHdrs->OptionalHeader.DataDirectory[0].VirtualAddress + 
                                        hMod);
  pFunctionNameVA = (DWORD *)(pExportDir->AddressOfNames + 
                             hMod);
 
  while(i <= pExportDir->NumberOfNames){
    if(lstrcmpA((char *)(*pFunctionNameVA + hMod), lpProcName) == 0){
      pOrdinal = (WORD *)(hMod + 
                          pExportDir->AddressOfNameOrdinals + 
                         (i * sizeof(WORD)));
      pAddress  = (DWORD *)(hMod + 
                            pExportDir->AddressOfFunctions + 
                           (sizeof(DWORD) * (*pOrdinal)));
 
      if((*pAddress >= ExportTableAddr) && (*pAddress <= ExportTableEnd)){
        i = 0;
        pDllName = (char *)(*pAddress + hMod);
        while(*(pDllName + i) != '.'){
          *(szDll + i) = *(pDllName + i);
          i++;
        }
 
        lstrcatA(szDll, ".dll");
        lstrcpyA(szFunc, pDllName + i + 1);
 
        return (FARPROC)MyGetProcAddress(LoadLibraryA(szDll), szFunc);
      } 
 
      return (FARPROC)(*pAddress + hMod);
    }
    i++;
    pFunctionNameVA++;
  }
 
  return NULL;
}
 
int main(){
  HMODULE hModule = LoadLibraryA("kernel32.dll");
  char *szFuncName = "EnterCriticalSection";
 
  printf("GetProcAddress   0x%p\n", GetProcAddress(hModule, szFuncName));
  printf("MyGetProcAddress 0x%p\n", MyGetProcAddress(hModule, szFuncName));
  getchar();
  return 0;
}

4. Odkazy
[1] – MSDN GetProcAddress

[2] – MSDN LoadLibrary

[3] – MSDN LoadLibraryEx

[4] – MSDN GetModuleHandle

[5] – pinvoke IMAGE_DOS_HEADER

[6] – MSDN IMAGE_NT_HEADERS

[7] – MSDN IMAGE_OPTIONAL_HEADER

[8] – MSDN IMAGE_DATA_DIRECTORY

[9] – pinvoke IMAGE_EXPORT_DIRECTORY

THXto: kernelhunter