giovedì 4 aprile 2019

Mappare DLL manualmente in un altro processo

In un precedente articolo (si veda [1]) si è visto come un processo possa caricare manualmente una DLL nascondendo la sua presenza tra i moduli di dipendenza. Più interessante sarebbe vedere come si possa caricare una DLL manualmente in un altro processo. Lo schema è praticamente lo stesso, l'unica differenza è che il processo iniettore può arrivare solo fino ad un certo punto: prima carica nel proprio spazio virtuale l'immagine su disco della DLL e successivamente scrive l'immagine in memoria della DLL in uno spazio allocato nel processo target. A questo punto non può fare nient'altro. A fixare IAT (import address table) e rilocazioni ci deve pensare il processo target. A questo scopo l'iniettore può caricare il codice del loader/fixer nello spazio virtuale del processo target e creare un thread remoto che eseguirà tale codice. L'immagine sotto descrive tale tecnica in modo semplificato.




Per quanto riguarda lo stile scelto per l'esposizione della tecnica presentata in questo articolo, ho deciso di scrivere tutte le info sotto forma di commenti al codice. Per non appesantire troppo il discorso, inoltre, si prenderà in considerazione e si spiegherà in dettaglio solo la parte relativa all'articolo. Tutto quanto necessario alla comprensione del resto del sorgente si può trovare in un articolo precedente linkato nei riferimenti sotto (si veda [1] e si faccia riferimento ad esso se si hanno dubbi). Il consiglio, per leggere il codice di questo articolo, è quello di partire dall'entrypoint (wmain) seguendo il flusso naturale delle istruzioni. L'unica cosa degna di nota da aggiungere è che questo codice funziona solo se l'iniettore è compilato in modalità Release. Pensavo fosse dovuto al fatto che in Visual Studio, di default, gli eseguibili vengono linkati con l'opzione \INCREMENTAL ma anche settandola a NO i risultati non sono cambiati. Non ho indagato in maniera approfondita quindi, almeno per il momento, è meglio evitare la modalità Debug e compilare esclusivamente in Release.


#include <iostream>
 
#include <Windows.h>
#include <TlHelp32.h>
 
// sopprime warning di sicurezza per alcune funzioni deprecate tipo wcscat
#pragma warning(disable : 4996)
 
using namespace std;
 
typedef HMODULE(WINAPIpLoadLibraryA)(LPCSTR);
typedef HMODULE(WINAPIpGetModuleHandleA)(LPCSTR);
typedef FARPROC(WINAPIpGetProcAddress)(HMODULELPCSTR);
 
typedef BOOL(WINAPIfpDllMain)(HMODULEDWORDLPVOID);
 
// Per non ripetere le cose due volte, una volta calcolate
// in iniettore, le info della DLL vengono salvate in questa struttura
// e scritte in spazio allocato in processo target insieme al Loader, che
// potrà utilizzarle direttamente senza ricalcolarle.
struct DLL_MAPPING_INFO
{
 ULONG_PTR ImageBase;
 
 PIMAGE_NT_HEADERS NtHeaders;
 PIMAGE_BASE_RELOCATION BaseReloc;
 PIMAGE_IMPORT_DESCRIPTOR ImportDirectory;
 
 pLoadLibraryA fnLoadLibraryA;
 pGetModuleHandleA fnGetModuleHandleA;
 pGetProcAddress fnGetProcAddress;
 
};
 
 
void GetAppPath(wchar_t *szCurFile)
{
 GetModuleFileName(0, szCurFileMAX_PATH);
 
 for (SIZE_T i = wcslen(szCurFile) - 1; i >= 0; i--)
 {
  if (szCurFile[i] == '\\')
  {
   szCurFile[i + 1] = '\0';
   break;
  }
 }
}


 
int main()
{
 // Percorso DLL
 wchar_t szDllFile[MAX_PATH] = { 0 };
 
 GetAppPath(szDllFile);
 std::wcscat(szDllFile, L"MySampleDLL.dll");
 
 // PID processo target
 DWORD ProcessId = GetProcessIdByProcessName(L"MyTarget.exe");
 
 DLL_MAPPING_INFO Dll_Info;
 
 HANDLE hFile = CreateFile(szDllFile, GENERIC_READFILE_SHARE_READ | FILE_SHARE_WRITENULL,
  OPEN_EXISTING, 0, NULL);
 
 DWORD FileSize = GetFileSize(hFile, NULL);
 
 LPVOID FileBuffer = VirtualAlloc(NULL, FileSize, MEM_COMMIT | MEM_RESERVEPAGE_READWRITE);
 
 // Salva immagine su disco della DLL in memoria appena allocata nel processo dell'iniettore
 ReadFile(hFile, FileBuffer, FileSize, NULLNULL);
 
 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)FileBuffer;
 PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileBuffer + pDosHeader->e_lfanew);
 
 // Handle al processo target
 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESSFALSE, ProcessId);
 
 // Alloca tanto spazio in processo target da contenere l'immagine in memoria della DLL
 LPVOID ExecutableImage = VirtualAllocEx(hProcess, NULL, pNtHeaders->OptionalHeader.SizeOfImage,
  MEM_COMMIT | MEM_RESERVEPAGE_EXECUTE_READWRITE);
 
 // Scrive headers in spazio appena allocato in processo target
 WriteProcessMemory(hProcess, ExecutableImage, FileBuffer,
  pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);
 
 PIMAGE_SECTION_HEADER pSectHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)pNtHeaders + sizeof(IMAGE_NT_HEADERS));
 
 // Scrive sezioni
 for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
 {
  if ((pSectHeader[i].VirtualAddress == 0) ||
   (pSectHeader[i].SizeOfRawData == 0))
  {
   ++i;
   continue;
  }
 
  WriteProcessMemory(
   hProcess, 
   (PVOID)((LPBYTE)ExecutableImage + pSectHeader[i].VirtualAddress),
   (PVOID)((LPBYTE)FileBuffer + pSectHeader[i].PointerToRawData), pSectHeader[i].SizeOfRawData, NULL);
 }
 
 // Alloca tanto spazio in processo target da contenere info della DLL (DLL_MAPPING_INFO) più
 // il codice del Loader necessario a fixare Import table e Relocations.
 LPVOID LoaderMemory = VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE,
  PAGE_EXECUTE_READWRITE);
 
 Dll_Info.ImageBase = (ULONG_PTR)ExecutableImage;
 Dll_Info.NtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)ExecutableImage + pDosHeader->e_lfanew);
 
 Dll_Info.BaseReloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)ExecutableImage
  + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
 Dll_Info.ImportDirectory = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)ExecutableImage
  + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
 
 Dll_Info.fnLoadLibraryA = LoadLibraryA;
 Dll_Info.fnGetModuleHandleA = GetModuleHandleA;
 Dll_Info.fnGetProcAddress = GetProcAddress;
 
 // Scrive le info della DLL nello spazio appena allocato nel processo target
 WriteProcessMemory(hProcess, LoaderMemory, &Dll_Info, sizeof(DLL_MAPPING_INFO), NULL);
 
 // Scrive il codice del Loader nello spazio appena allocato nel processo target
 // Sfrutta aritmetica dei puntatori per calcolare dimensione di LibraryLoader
 WriteProcessMemory(hProcess, (LPVOID)((LPBYTE)LoaderMemory + sizeof(DLL_MAPPING_INFO)), LibraryLoader,
  (ULONG_PTR)LL_end - (ULONG_PTR)LibraryLoader, NULL);
 
 // Crea thread remoto per eseguire Loader nel processo target
 HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, 
  (LPTHREAD_START_ROUTINE)((LPBYTE)LoaderMemory + sizeof(DLL_MAPPING_INFO)), LoaderMemory, 0, NULL);
 
 // Attende che il Loader termini
 WaitForSingleObject(hThread, INFINITE);
 
 std::cout << "Loader has been completed successfully!" << std::endl;
 std::cout << "Press enter to Exit!" << std::endl;
 std::cin.get();
 
 // Libera spazio allocato in processo target
 VirtualFreeEx(hProcess, LoaderMemory, 0, MEM_RELEASE);
 
 return 0;
}

void ShowError(const wchar_t *pszText)
{
 wchar_t szErr[MAX_PATH] = { 0 };
 wsprintf(szErr, L"%s Error[%d]\n"pszText, GetLastError());
 MessageBox(NULL, szErr, L"ERROR"MB_OK);
}

DWORD GetProcessIdByProcessName(const wchar_t *pszProcessName)
{
 DWORD dwProcessId = 0;
 PROCESSENTRY32 pe32 = { 0 };
 HANDLE hSnapshot = NULL;
 BOOL bRet = FALSE;
 RtlZeroMemory(&pe32, sizeof(pe32));
 pe32.dwSize = sizeof(pe32);
 
 hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
 if (NULL == hSnapshot)
 {
  ShowError(L"CreateToolhelp32Snapshot");
  return dwProcessId;
 }
 
 bRet = Process32First(hSnapshot, &pe32);
 while (bRet)
 {
  if (0 == lstrcmpi(pe32.szExeFile, pszProcessName))
  {
   dwProcessId = pe32.th32ProcessID;
   break;
  }
 
  bRet = Process32Next(hSnapshot, &pe32);
 }
 
 return dwProcessId;
}

// Si occupa di fixare rilocazioni e import table della DLL ma,
// siccome la DLL è caricata nello spazio di un altro processo,
// questo codice deve essere eseguito nello spazio di tale processo.
DWORD LibraryLoader(LPVOID Memory)
{
 DLL_MAPPING_INFO* Dll_Info = (DLL_MAPPING_INFO*)Memory;
 
 PIMAGE_BASE_RELOCATION pLoc = Dll_Info->BaseReloc;
 
 LONGLONG dwDelta = Dll_Info->ImageBase - Dll_Info->NtHeaders->OptionalHeader.ImageBase;
 
 // Fixa rilocazioni
 while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0)
 {
  WORD *pLocData = (WORD *)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION));
  int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
 
  for (int i = 0; i < nNumberOfReloc; i++)
  {
   if ((pLocData[i] & 0xF000) == 0xA000)
   {
    PULONG_PTR pAddress = (PULONG_PTR)(Dll_Info->ImageBase + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF));
    *pAddress += dwDelta;
 
   }
  }
 
  pLoc = (PIMAGE_BASE_RELOCATION)((LPBYTE)pLoc + pLoc->SizeOfBlock);
 }
 
 PIMAGE_IMPORT_DESCRIPTOR pImportTable = Dll_Info->ImportDirectory;
 
 // Fixa Import table
 while (pImportTable->Characteristics)
 {
  PIMAGE_THUNK_DATA OrigFirstThunk = (PIMAGE_THUNK_DATA)(Dll_Info->ImageBase + pImportTable->OriginalFirstThunk);
  PIMAGE_THUNK_DATA FirstThunk = (PIMAGE_THUNK_DATA)(Dll_Info->ImageBase + pImportTable->FirstThunk);
 
  char *lpDllName = (char *)(Dll_Info->ImageBase + pImportTable->Name);
  HMODULE hModule = Dll_Info->fnGetModuleHandleA(lpDllName);
  if (hModule == NULL)
  {
   // usa LoadLibraryA per caricare tale modulo se non è già stato caricato
   hModule = Dll_Info->fnLoadLibraryA(lpDllName);
   if (hModule == NULL)
   {
    pImportTable++;
    continue;
   }
  }
 
  while (OrigFirstThunk->u1.AddressOfData)
  {
   FARPROC Function;
 
   if (OrigFirstThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
   {
    // Importa attraverso ordinale
    Function = Dll_Info->fnGetProcAddress(hModule, (LPCSTR)(OrigFirstThunk->u1.Ordinal - IMAGE_ORDINAL_FLAG));
   }
   else
   {
    // Importa attraverso nome
    PIMAGE_IMPORT_BY_NAME lpImportByName = (PIMAGE_IMPORT_BY_NAME)((LPBYTE)Dll_Info->ImageBase + 
        OrigFirstThunk->u1.AddressOfData);
    Function = Dll_Info->fnGetProcAddress(hModule, (LPCSTR)lpImportByName->Name);
   }
 
   FirstThunk->u1.Function = (ULONG_PTR)Function;
 
   OrigFirstThunk++;
   FirstThunk++;
  }
 
  pImportTable++;
 }
 
 // Chiama entrypoint
 if (Dll_Info->NtHeaders->OptionalHeader.AddressOfEntryPoint)
 {
  fpDllMain EntryPoint = (fpDllMain)((LPBYTE)Dll_Info->ImageBase + Dll_Info->NtHeaders->OptionalHeader.AddressOfEntryPoint);
  return EntryPoint((HMODULE)Dll_Info->ImageBase, DLL_PROCESS_ATTACHNULL);
 }
 
 return TRUE;
}

// serve solo a calcolare dimensione in byte di LibraryLoader
DWORD LL_end()
{
 return 0;
}



Anche in questo caso la DLL non verrà elencata tra i moduli di dipendenze del processo target in quanto non ha il relativo descriptor.





Codice sorgente:
Manual_DLL_Injector.zip




Riferimenti:
[1] Caricare DLL in memoria manualmente
[2] https://github.com/Zer0Mem0ry/ManualMap

Nessun commento:

Posta un commento