1.15 自實現GetProcAddress

2023-09-04 12:01:40

在正常情況下,要想使用GetProcAddress函數,需要首先呼叫LoadLibraryA函數獲取到kernel32.dll動態連結庫的記憶體地址,接著在呼叫GetProcAddress函數時傳入模組基址以及模組中函數名即可動態獲取到特定函數的記憶體地址,但在有時這個函數會被保護起來,導致我們無法直接呼叫該函數獲取到特定函數的記憶體地址,此時就需要自己編寫實現LoadLibrary以及GetProcAddress函數,該功能的實現需要依賴於PEB執行緒環境塊,通過執行緒環境塊可遍歷出kernel32.dll模組的入口地址,接著就可以在該模組中尋找GetProcAddress函數入口地址,當找到該入口地址後即可直接呼叫實現動態定位功能。

首先通過PEB/TEB找到自身程序的所有載入模組資料,獲取TEB也就是執行緒環境塊。在程式設計的時候TEB始終儲存在暫存器 FS 中。

0:000> !teb
TEB at 00680000
    ExceptionList:        008ff904
    StackBase:            00900000
    StackLimit:           008fc000
    RpcHandle:            00000000
    Tls Storage:          0068002c
    PEB Address:          0067d000

0:000> dt _teb 00680000
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : (null) 
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : (null) 
   +0x02c ThreadLocalStoragePointer : 0x0068002c Void
   +0x030 ProcessEnvironmentBlock : 0x0067d000 _PEB      // 偏移為30,PEB

從該命令的輸出可以看出,PEB 結構體的地址位於 TEB 結構體偏移0x30 的位置,該位置儲存的地址是 0x0067d000。也就是說,PEB 的地址是 0x0067d000,通過該地址來解析 PEB並獲得 LDR結構。

0:000> dt nt!_peb 0x0067d000
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x00f30000 Void
   +0x00c Ldr              : 0x774c0c40 _PEB_LDR_DATA    // LDR

從如上輸出結果可以看出,LDRPEB 結構體偏移的 0x0C 處,該地址儲存的地址是 0x774c0c40 通過該地址來解析 LDR 結構體。WinDBG 輸出如下內容:

0:000> dt _peb_ldr_data 0x774c0c40
ntdll!_PEB_LDR_DATA
   +0x000 Length           : 0x30
   +0x004 Initialized      : 0x1 ''
   +0x008 SsHandle         : (null) 
   +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x9e3208 - 0x9e5678 ]
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x9e3210 - 0x9e5680 ]
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x9e3110 - 0x9e35f8 ]
   +0x024 EntryInProgress  : (null) 
   +0x028 ShutdownInProgress : 0 ''
   +0x02c ShutdownThreadId : (null) 

0:000> dt _LIST_ENTRY
ntdll!_LIST_ENTRY
   +0x000 Flink            : Ptr32 _LIST_ENTRY
   +0x004 Blink            : Ptr32 _LIST_ENTRY

現在來手動遍歷第一條連結串列,輸入命令0x9e3208:在連結串列偏移 0x18 的位置是模組的對映地址,即 ImageBase;在連結串列
偏移 0x28 的位置是模組的路徑及名稱的地址;在連結串列偏移 0x30 的位置是模組名稱的地址。

0:000> dd 0x9e3208
009e3208  009e3100 774c0c4c 009e3108 774c0c54
009e3218  00000000 00000000 00f30000 00f315bb
009e3228  00007000 00180016 009e1fd4 00120010
009e3238  009e1fda 000022cc 0000ffff 774c0b08

0:000> du 009e1fd4
009e1fd4  "C:\main.exe"
0:000> du 009e1fda
009e1fda  "main.exe"

讀者可自行驗證,如下所示的確是模組的名稱。既然是連結串列,就來下一條連結串列的資訊,009e3100儲存著下一個連結串列結構。依次遍歷就是了。

0:000> dd 009e3100
009e3100  009e35e8 009e3208 009e35f0 009e3210
009e3110  009e39b8 774c0c5c 773a0000 00000000
009e3120  0019c000 003c003a 009e2fe0 00140012

0:000> du 009e2fe0 
009e2fe0  "C:\Windows\SYSTEM32\ntdll.dll"

上述地址009e3100介紹的結構,是微軟保留結構,只能從網上找到一個結構定義,然後自行看著解析就好了。

typedef struct _LDR_DATA_TABLE_ENTRY
{
  PVOID Reserved1[2];
  LIST_ENTRY InMemoryOrderLinks;
  PVOID Reserved2[2];
  PVOID DllBase;
  PVOID EntryPoint;
  PVOID Reserved3;
  UNICODE_STRING FullDllName;
  BYTE Reserved4[8];
  PVOID Reserved5[3];
  union {
  ULONG CheckSum;
  PVOID Reserved6;
  };
  ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; 

根據如上流程,想要得到kernel32.dll模組的入口地址,我們可以進行這幾步,首先得到TEB地址,並在該地址中尋找PEB執行緒環境塊,並在該環境塊內得到LDR結構,在該結構中獲取第二條連結串列地址,輸出該連結串列中的0x10以及0x20即可得到當前模組的基地址,以及完整的模組路徑資訊,該功能的實現分為32位元於64位元,如下程式碼則是實現程式碼。

#include <iostream>
#include <Windows.h>

// 將不同的節壓縮為單一的節
#pragma comment(linker, "/merge:.data=.text") 
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/section:.text,RWE")

// 得到32位元模式下kernel32.dll地址
DWORD GetModuleKernel32()
{
  DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL;
  DWORD *BaseAddress = NULL, *FullDllName = NULL;

  __asm
  {
    mov eax, fs:[0x30]      // FS儲存著TEB
    mov PEB, eax            // +30定位到PEB
  }

  // 得到LDR
  Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c));

  // 在LDR基礎上找到第二條連結串列
  Flink = *((DWORD **)((unsigned char *)Ldr + 0x14));
  p = Flink;

  p = *((DWORD **)p);

  // 計數器
  int count = 0;

  while (Flink != p)
  {
    BaseAddress = *((DWORD **)((unsigned char *)p + 0x10));
    FullDllName = *((DWORD **)((unsigned char *)p + 0x20));

    if (BaseAddress == 0)
      break;

    // printf("映象基址 = %08x \r\n 模組路徑 = %S \r\n", BaseAddress, (unsigned char *)FullDllName);
    // 第二個模組是kernel32.dll
    if (count == 1)
    {
      // printf("address =%x \n", BaseAddress);
      return reinterpret_cast<DWORD>(BaseAddress);
    }

    p = *((DWORD **)p);
    count = count + 1;
  }

  // 未找到Kernel32模組
  return 0;
}

// 獲取64位元模式下的kernel32.dll基址
ULONGLONG GetModuleKernel64()
{
  ULONGLONG dwKernel32Addr = 0;

  // 獲取TEB的地址
  _TEB* pTeb = NtCurrentTeb();
  // 獲取PEB的地址
  PULONGLONG pPeb = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pTeb + 0x60);
  // 獲取PEB_LDR_DATA結構的地址
  PULONGLONG pLdr = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pPeb + 0x18);
  // 模組初始化連結串列的頭指標InInitializationOrderModuleList
  PULONGLONG pInLoadOrderModuleList = (PULONGLONG)((ULONGLONG)pLdr + 0x10);

  // 獲取連結串列中第一個模組資訊,exe模組
  PULONGLONG pModuleExe = (PULONGLONG)*pInLoadOrderModuleList;
  //printf("EXE Base = > %X \n", pModuleExe[6]);

  // 獲取連結串列中第二個模組資訊,ntdll模組
  PULONGLONG pModuleNtdll = (PULONGLONG)*pModuleExe;
  //printf("Ntdll Base = > %X \n", pModuleNtdll[6]);

  // 獲取連結串列中第三個模組資訊,Kernel32模組
  PULONGLONG pModuleKernel32 = (PULONGLONG)*pModuleNtdll;
  //printf("Kernel32 Base = > %X \n", pModuleKernel32[6]);

  // 獲取kernel32基址
  dwKernel32Addr = pModuleKernel32[6];
  return dwKernel32Addr;
}

int main(int argc, char *argv[])
{
  // 輸出32位元kernel32
  DWORD kernel32BaseAddress = GetModuleKernel32();
  std::cout << "kernel32 = " << std::hex << kernel32BaseAddress << std::endl;

  // 輸出64位元kernel32
  ULONGLONG kernel64BaseAddress = GetModuleKernel64();
  std::cout << "kernel64 = " << std::hex << kernel32BaseAddress << std::endl;

  system("pause");
  return 0;
}

如上程式碼中分別實現了32位於64位兩種獲取記憶體模組基址GetModuleKernel32用於獲取32位元模式,GetModuleKernel64則用於獲取64位元記憶體基址,讀者可自行呼叫兩種模式,輸出如下圖所示;

我們通過呼叫GetModuleKernel32()函數讀入kernel32.dll模組入口地址後,則下一步就可以通過迴圈,遍歷該模組的匯出表並尋找到GetProcAddress匯出函數地址,找到該匯出函數記憶體地址後,則可以通過kernel32模組基址加上dwFunAddrOffset相對偏移,獲取到該函數的記憶體地址,此時通過函數指標就可以將該函數地址讀入到記憶體指標內。

// 封裝基地址獲取功能
ULONGLONG MyGetProcAddress()
{
  // 獲取32位元基址
  ULONGLONG dwBase = GetModuleKernel32();
  
  // 獲取64位元基址
  // ULONGLONG dwBase = GetModuleKernel64();
  
  // 獲取DOS頭
  PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwBase;
  
  // 獲取32位元NT頭
  PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(dwBase + pDos->e_lfanew);
  
  // 獲取64位元NT頭
  // PIMAGE_NT_HEADERS64  pNt = (PIMAGE_NT_HEADERS64)(dwBase + pDos->e_lfanew);
  
  // 獲取資料目錄表
  PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory;
  pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]);
  DWORD dwOffset = pExportDir->VirtualAddress;
  
  // 獲取匯出表資訊結構
  PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwBase + dwOffset);
  DWORD dwFunCount = pExport->NumberOfFunctions;
  DWORD dwFunNameCount = pExport->NumberOfNames;
  DWORD dwModOffset = pExport->Name;

  // 獲取匯出地址表
  PDWORD pEAT = (PDWORD)(dwBase + pExport->AddressOfFunctions);
  
  // 獲取匯出名稱表
  PDWORD pENT = (PDWORD)(dwBase + pExport->AddressOfNames);
  
  // 獲取匯出序號表
  PWORD pEIT = (PWORD)(dwBase + pExport->AddressOfNameOrdinals);

  for (DWORD dwOrdinal = 0; dwOrdinal < dwFunCount; dwOrdinal++)
  {
    if (!pEAT[dwOrdinal])
    {
      continue;
    }

    // 獲取序號
    DWORD dwID = pExport->Base + dwOrdinal;
    
    // 獲取匯出函數地址
    ULONGLONG dwFunAddrOffset = pEAT[dwOrdinal];

    for (DWORD dwIndex = 0; dwIndex < dwFunNameCount; dwIndex++)
    {
      // 在序號表中查詢函數的序號
      if (pEIT[dwIndex] == dwOrdinal)
      {
        // 根據序號索引到函數名稱表中的名字
        ULONGLONG dwNameOffset = pENT[dwIndex];
        char* pFunName = (char*)((ULONGLONG)dwBase + dwNameOffset);
        if (!strcmp(pFunName, "GetProcAddress"))
        {
          // 根據函數名稱返回函數地址
          return dwBase + dwFunAddrOffset;
        }
      }
    }
  }
  return 0;
}

// 定義名稱指標
typedef ULONGLONG(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);

int main(int argc, char *argv[])
{

  DWORD kernel32BaseAddress = GetModuleKernel32();
  if (kernel32BaseAddress == 0)
  {
    return 0;
  }

  // 獲取kernel32基址/獲取GetProcAddress的基址
  fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
  std::cout << pfnGetProcAddress << std::endl;

  // 獲取Kernel32核心API地址
  fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress((HMODULE)kernel32BaseAddress, "LoadLibraryA");
  printf("自定義讀入LoadLibrary = %x \n", pfnLoadLibraryA);

  system("pause");
  return 0;
}

輸出效果如下圖所示,我們即可讀入fnLoadLibraryA函數的記憶體地址;

上述程式碼的使用也很簡單,當我們能夠得到GetProcAddress的記憶體地址後,就可以使用該記憶體地址動態定位到任意一個函數地址,我們通過得到LoadLibrary函數地址,與GetModuleHandleA函數地址,通過兩個函數就可以定位到Windows系統內任意一個函數,我們以呼叫MessageBox彈窗為例,動態輸出一個彈窗,該呼叫方式如下所示。

// 定義名稱指標
typedef ULONGLONG(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);
typedef int(WINAPI *fnMessageBox)(HWND hWnd, LPSTR lpText, LPSTR lpCaption, UINT uType);
typedef HMODULE(WINAPI *fnGetModuleHandleA)(_In_opt_ LPCSTR lpModuleName);
typedef BOOL(WINAPI *fnVirtualProtect)(_In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect);
typedef void(WINAPI *fnExitProcess)(_In_ UINT uExitCode);

int main(int argc, char * argv[])
{
  // 獲取kernel32基址 / 獲取GetProcAddress的基址
  fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
  ULONGLONG dwBase = GetModuleKernel32();
  printf("fnGetProcAddress = %x \n", pfnGetProcAddress);
  printf("GetKernel32Addr = %x \n", dwBase);

  // 獲取Kernel32核心API地址
  fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress((HMODULE)dwBase, "LoadLibraryA");
  printf("pfnLoadLibraryA = %x \n", pfnLoadLibraryA);

  fnGetModuleHandleA pfnGetModuleHandleA = (fnGetModuleHandleA)pfnGetProcAddress((HMODULE)dwBase, "GetModuleHandleA");
  printf("pfnGetModuleHandleA = %x \n", pfnGetModuleHandleA);

  fnVirtualProtect pfnVirtualProtect = (fnVirtualProtect)pfnGetProcAddress((HMODULE)dwBase, "VirtualProtect");
  printf("pfnVirtualProtect = %x \n", pfnVirtualProtect);

  // 有了核心API之後,即可獲取到User32.dll的基地址
  pfnLoadLibraryA("User32.dll");
  HMODULE hUser32 = (HMODULE)pfnGetModuleHandleA("User32.dll");
  fnMessageBox pfnMessageBoxA = (fnMessageBox)pfnGetProcAddress(hUser32, "MessageBoxA");
  printf("User32 = > %x \t MessageBox = > %x \n", hUser32, pfnMessageBoxA);

  HMODULE hKernel32 = (HMODULE)pfnGetModuleHandleA("kernel32.dll");
  fnExitProcess pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
  printf("Kernel32 = > %x \t ExitProcess = > %x \n", hKernel32, pfnExitProcess);

  // 彈出資訊框
  int nRet = pfnMessageBoxA(NULL, "hello lyshark", "MsgBox", MB_YESNO);
  if (nRet == IDYES)
  {
    printf("你點選了YES \n");
  }

  system("pause");
  pfnExitProcess(0);
  return 0;
}

執行上述程式碼,通過動態呼叫的方式獲取到MessageBox函數記憶體地址,並將該記憶體放入到pfnMessageBoxA指標內,最後直接呼叫該指標即可輸出如下圖所示的效果圖;