giovedì 11 aprile 2019

DLL injection tramite ShellCode

In un altro articolo (si veda [1]) si è visto come è possibile iniettare una DLL in un processo remoto semplicemente caricando DLL e parte del loader (quella che si occupa di fixare la DLL in memoria) in tale processo e creando un nuovo thread, sempre nello stesso, che eseguisse il codice del loader/fixer . Un approccio leggermente diverso (affrontato in questo articolo) ma che porta allo stesso risultato è quello di caricare del codice assembly in forma di array di byte, noto come shellcode, da eseguire nel target. Per farlo, uno dei tanti modi disponibili è quello di sospendere il thread principale del target e forzarlo a riprendere dall'inizio dello shellcode, che quindi provvederà a caricare la DLL chiamando semplicemente LoadLibrary. 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 [2] 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 importante da aggiungere è che questo codice sorgente si basa su un tutorial presente su GuidedHacking (si veda [3]).

Di seguito il codice dell'iniettore:

int wmain(int argcwchar_t * argv[])
{
 // Percorso DLL
 WCHAR szDllFile[MAX_PATH] = { 0 };
 
 GetAppPath(szDllFile);
 std::wcscat(szDllFile, L"SampleDLL.dll");
 
 InjectDll(L"Target.exe", szDllFile);
 
 std::cout << "Press enter to exit" << std::endl;
 std::cin.get();
 
 return 0;
}

void GetAppPath(LPWSTR szCurFile)
{
 GetModuleFileName(0, szCurFileMAX_PATH);
 
 for (SIZE_T i = wcslen(szCurFile) - 1; i >= 0; i--)
 {
  if (szCurFile[i] == '\\')
  {
   szCurFile[i + 1] = '\0';
   break;
  }
 }
}

bool InjectDll(LPCWSTR szProcessLPCWSTR szPath)
{
 HANDLE hProc = GetProcessHandleByProcessName(szProcess);
 if (!hProc)
 {
  ShowError(L"GetProcessHandleByProcessName");
  return false;
 }
 
 SIZE_T len = wcslen(szPath) * sizeof(WCHAR);
 LPVOID pArg = VirtualAllocEx(hProc, nullptr, len, MEM_COMMIT | MEM_RESERVEPAGE_EXECUTE_READWRITE);
 if (!pArg)
 {
  ShowError(L"VirtualAllocEx");
  CloseHandle(hProc);
  return false;
 }
 
 BOOL bRet = WriteProcessMemory(hProc, pArg, szPath, len, nullptr);
 if (!bRet)
 {
  ShowError(L"WriteProcessMemory");
  VirtualFreeEx(hProc, pArg, 0, MEM_RELEASE);
  CloseHandle(hProc);
  return false;
 }
 
 pLoadLibraryW p_LoadLibrary = LoadLibraryW;
 if (!p_LoadLibrary)
 {
  ShowError(L"GetProcAddressEx");
  VirtualFreeEx(hProc, pArg, 0, MEM_RELEASE);
  CloseHandle(hProc);
  return false;
 }
 
 UINT_PTR hDllOut = 0;
 DWORD last_error = 0;
 DWORD dwErr = HijackThread(hProc, p_LoadLibrary, pArg, last_error, hDllOut);
 
 CloseHandle(hProc);
 
 if (dwErr)
 {
  std::cout << std::hex << "StartRoutine failed: 0x" << dwErr << std::endl;
  std::cout << std::hex << " LastWin32Error: 0x" << last_error << std::endl;
 
  return false;
 }
 
 std::cout << std::hex << "Success!  LoadLibrary returned 0x" << hDllOut << std::endl;
 return true;
}

HANDLE GetProcessHandleByProcessName(LPCWSTR 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 (hSnapshot == NULL)
 {
  ShowError(L"CreateToolhelp32Snapshot");
 }
 
 bRet = Process32First(hSnapshot, &pe32);
 while (bRet)
 {
  if (0 == lstrcmpi(pe32.szExeFile, pszProcessName))
  {
   dwProcessId = pe32.th32ProcessID;
   break;
  }
 
  bRet = Process32Next(hSnapshot, &pe32);
 }
 
 return OpenProcess(PROCESS_ALL_ACCESSFALSE, dwProcessId);
}


DWORD HijackThread(HANDLE hTargetProcpLoadLibraryW pRoutineLPVOID pArgDWORD &LastWin32ErrorUINT_PTR &HMDll)
{
 // Ottiene thread ID eseguito in Target
 THREADENTRY32 TE32{ 0 };
 TE32.dwSize = sizeof(TE32);
 
 HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetProcessId(hTargetProc));
 if (hSnap == INVALID_HANDLE_VALUE)
 {
  LastWin32Error = GetLastError();
 
  return SR_HT_ERR_TH32_FAIL;
 }
 
 DWORD dwTargetPID = GetProcessId(hTargetProc);
 DWORD ThreadID = 0;
 
 BOOL bRet = Thread32First(hSnap, &TE32);
 if (!bRet)
 {
  LastWin32Error = GetLastError();
 
  CloseHandle(hSnap);
 
  return SR_HT_ERR_T32FIRST_FAIL;
 }
 
 do
 {
  if (TE32.th32OwnerProcessID == dwTargetPID)
  {
   ThreadID = TE32.th32ThreadID;
   break;
  }
  
  bRet = Thread32Next(hSnap, &TE32);
 
 } while (bRet);
 
 if (!ThreadID)
 {
  return SR_HT_ERR_NO_THREADS;
 }
 
 // Ottiene handle al thread eseguito in Target
 HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUMEFALSE, ThreadID);
 if (!hThread)
 {
  LastWin32Error = GetLastError();
 
  return SR_HT_ERR_OPEN_THREAD_FAIL;
 }
 
 // Sospende il thread eseguito in Target
 if (SuspendThread(hThread) == (DWORD)-1)
 {
  LastWin32Error = GetLastError();
 
  CloseHandle(hThread);
 
  return SR_HT_ERR_SUSPEND_FAIL;
 }
 
 // Ottiene il contesto del thread eseguito, e ora sospeso, in Target.
 // Dentro il contesto ci sono info utili tra cui l'istrunction pointer (registro RIP)
 // che ci serve sia per saltare allo shellcode che per
 // riprendere l'esecuzione da dove era stata sospesa.
 CONTEXT OldContext{ 0 };
 OldContext.ContextFlags = CONTEXT_CONTROL;
 
 if (!GetThreadContext(hThread, &OldContext))
 {
  LastWin32Error = GetLastError();
 
  ResumeThread(hThread);
  CloseHandle(hThread);
 
  return SR_HT_ERR_GET_CONTEXT_FAIL;
 }
 
 // Alloca spazio per lo shellcode in Target
 LPVOID pCodecave = VirtualAllocEx(hTargetProcnullptr, 0x100, MEM_COMMIT | MEM_RESERVEPAGE_EXECUTE_READWRITE);
 if (!pCodecave)
 {
  LastWin32Error = GetLastError();
 
  ResumeThread(hThread);
  CloseHandle(hThread);
 
  return SR_HT_ERR_CANT_ALLOC_MEM;
 }
 
 // SPIEGAZIONE SHELLCODE:
 // Primi 8 byte (returned value) servirà dopo per conservare HMODULE (indirizzo di base) della DLL. 
 
 // Prima vera istruzione da eseguire è:
 // sub rsp, 0x08 alloca spazio sullo stack e servirà per salvare il valore di ritorno 
 // quando il codice dello Shellcode verrà completato. Con il ret finale, infatti, tale valore 
 // andrà automaticamente in RIP dato che sarà il valore puntato da RSP in quel momento.
 
 // A tale scopo, i due mov successivi salvano valore di RIP, al momento della sospensione del thread, 
 // proprio nello spazio allocato sullo stack in precedenza da sub rsp, 0x08.
 
 // I due push successivi salvano alcuni registri (pushfq salva registro RFLAGS) che verranno
 // usati dallo shellcode e che devono essere preservati in quanto potrebbe farne uso anche il
 // il codice in esecuzione al momento della sospensione del thread.
 
 // mov rax, pRoutine mette indirizzo di LoadLibrary in RAX.
 // mov rcx, pArg mette indirizzo di percorso della DLL in RCX.
 
 // sub rsp, 0x20 perchè stack deve essere allineato a 16 byte prima di ogni chiamata.
 // La prima istruzione (sub rsp, 0x08) ha disallineato lo stack di 8 byte.
 // Aggiungendo 0x20 = 32 siamo a 40 = 0x28. Ricordando che la call implica un push
 // del valore di ritorno sullo stack siamo a 48 = 0x30 che è proprio allineato a 16.
 
 // call rax chiama LoadLibrary passando il percorso della DLL da caricare
 
 // lea rcx, [pCodecave] mette indirizzo Shellcode in RCX:
 // 0x48, 0x8D, 0x0D, 0xB4, 0xFF, 0xFF, 0xFF, significa: lea rcx, [rip+0xFFFFFFB4] 
 // che vuol dire: lea rcx, [rip-76] che vuol dire: lea rcx, [rip-0x4C] con RIP che al
 // momento  di questa istruzione è (Shellcode + 0x44), che sommati agli 0x8 byte prima della
 // prima istruzione fanno 0x4C e quindi rip-0x4C è indirizzo di Shellcode.
 
 // mov [rcx], rax mette l'handle del modulo caricato nei primi 8 byte di shellcode (vedi returned value).
 
 // mov byte ptr[$ - 0x57], 0 mette zero in byte [$ - 0x57] per indicare che Shellcode  ha finito;
 // $ indica un RIP-relative addressing e al momento dell'istruzione RIP è a uguale a (Shellcode + 0x8 + 0x5A)
 // sottraendo 0x57 sia ha RIP - 0x57 = Shellcode + 0x8 + 0x5A - 0x57 = Shellcode + 0x8 + 0x3
 // che è l'ultimo byte dell'istruzione sub rsp, 0x08; tanto non serve più, quel che doveva fare (allocare 8 byte
 // su stack) l'ha già fatto.
 
 // Come già accennato, al momento del ret finale RSP punta allo spazio allocato sullo stack da
 // sub rsp, 0x08 e dove ora c'è il RIP originale del thread prima che avenisse la sospensione e
 // che così considerato il valore di ritorno di shellcode. In questo modo il thread riprende da dove
 //era stato sospeso.
 BYTE Shellcode[] =
 {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,                   // - 0x08   -> returned value
    
  0x48, 0x83, 0xEC, 0x08,                                           // + 0x00   -> sub rsp, 0x08
 
  0xC7, 0x04, 0x24, 0x00, 0x00, 0x00, 0x00,                         // + 0x04 (+ 0x07) -> mov [rsp], RipLowPart
  0xC7, 0x44, 0x24, 0x04, 0x00, 0x00, 0x00, 0x00,                   // + 0x0B (+ 0x0F) -> mov [rsp + 0x04], RipHighPart
 
  0x50, 0x51, 0x52, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, // + 0x13   -> push r(a/c/d)x / r(8 - 11)
  0x9C,                                                             // + 0x1E   -> pushfq
 
  0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,       // + 0x1F (+ 0x21) -> mov rax, pRoutine
  0x48, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,       // + 0x29 (+ 0x2B) -> mov rcx, pArg
 
  0x48, 0x83, 0xEC, 0x20,                                           // + 0x33   -> sub rsp, 0x20
  0xFF, 0xD0,                                                       // + 0x37   -> call rax
  0x48, 0x83, 0xC4, 0x20,                                           // + 0x39   -> add rsp, 0x20
 
  0x48, 0x8D, 0x0D, 0xB4, 0xFF, 0xFF, 0xFF,                         // + 0x3D   -> lea rcx, [pCodecave]
  0x48, 0x89, 0x01,                                                 // + 0x44   -> mov [rcx], rax
 
  0x9D,                                                             // + 0x47   -> popfq
  0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5A, 0x59, 0x58, // + 0x48   -> pop r(11-8) / r(d/c/a)x
  
  0xC6, 0x05, 0xA9, 0xFF, 0xFF, 0xFF, 0x00,                         // + 0x53   -> mov byte ptr[$ - 0x57], 0
 
  0xC3                                                              // + 0x5A   -> ret
 };                                                                 // SIZE = 0x5B (+ 0x08)
 
 // Offset da shellcode a inizio codice da eseguire
 DWORD FuncOffset  = 0x08;
 // Offset da shellcode a byte da settare in mov byte ptr[$ - 0x57], 0
 DWORD CheckByteOffset = 0x03 + FuncOffset;
 
 // Salva RIP da constesto del thread in shellcode.
 // In questo modo, quando l'esecuzione dello shellcode terminerà, il thread potrà riprendere
 // da dove era stato sospeso.
 DWORD dwLoRIP = (DWORD)(OldContext.Rip & 0xFFFFFFFF);
 DWORD dwHiRIP = (DWORD((OldContext.Rip) >> 0x20) & 0xFFFFFFFF);
 
 *(PDWORD)(Shellcode + 0x07 + FuncOffset) = dwLoRIP;
 *(PDWORD)(Shellcode + 0x0F + FuncOffset) = dwHiRIP;
 
 // Salva indirizzi di LoadLibraryW e percorso DLL in shellcode
 *(PULONG_PTR)(Shellcode + 0x21 + FuncOffset) = (ULONG_PTR)pRoutine;
 *(PULONG_PTR)(Shellcode + 0x2B + FuncOffset) = (ULONG_PTR)pArg;
 
 // Imposta RIP di contesto del thread con prima istruzione di shellcode in modo da 
 // alterare il flusso di esecuzione.
 OldContext.Rip = (UINT_PTR)(pCodecave) + FuncOffset;
 
 // Scrive shellcode in spazio allocato in precedenza in Target
 if (!WriteProcessMemory(hTargetProc, pCodecave, Shellcode, sizeof(Shellcode), nullptr))
 {
  LastWin32Error = GetLastError();
 
  ResumeThread(hThread);
  CloseHandle(hThread);
  VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE);
 
  return SR_HT_ERR_WPM_FAIL;
 }
 
 // Imposta contesto alerato
 if (!SetThreadContext(hThread, &OldContext))
 {
  LastWin32Error = GetLastError();
 
  ResumeThread(hThread);
  CloseHandle(hThread);
  VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE);
 
  return SR_HT_ERR_SET_CONTEXT_FAIL;
 }
 
 // Riprende thread sospeso
 if (ResumeThread(hThread) == (DWORD)-1)
 {
  LastWin32Error = GetLastError();
 
  CloseHandle(hThread);
  VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE);
 
  return SR_HT_ERR_RESUME_FAIL;
 }
 
 CloseHandle(hThread);
 
 // Aspetta per 5 secondi controllando che il byte dello shellcode venga azzerato.
 DWORD Timer  = GetTickCount();
 BYTE CheckByte = 1;
 
 do
 {
  ReadProcessMemory(hTargetProc, (LPBYTE)(pCodecave) + CheckByteOffset, &CheckByte, 1, nullptr);
 
  if (GetTickCount() - Timer > SR_REMOTE_TIMEOUT)
  {
   return SR_HT_ERR_TIMEOUT;
  }
 
  Sleep(10);
 } while (CheckByte != 0);
 
 // Passa HMODULE di DLL a paramentro di output HMDll
 ReadProcessMemory(hTargetProc, pCodecave, &HMDllsizeof(HMDll), nullptr);
 VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE);
 return SR_ERR_SUCCUESS;
}

Come si può notare, in Target è presente la DLL caricata dallo shellcode.








Codice sorgente:
DLL_Injector_Shellcode.zip




Riferimenti:
[1] Mappare DLL manualmente in un altro processo
[2] Caricare DLL in memoria manualmente
[3] https://guidedhacking.com/threads/c-shellcode-injection-tutorial.12132/

Nessun commento:

Posta un commento