
Motore di hooking tramite breakpoint hardware per Windows che usa i registri di debug per agganciare funzioni, bypassare ETW/AMSI ed eludere il monitoraggio EDR in user-land.
Questo articolo era originariamente per VX-Underground Black Mass Halloween Edition 2022.
Motori di hooking:
Tecnica generica di evasione user-land x64 che utilizza i registri di debug:
Esempi di hook ETW/AMSI disponibili
Il nostro compito è agganciare funzioni in modo banale e deviare il flusso del codice secondo necessità, e infine rimuovere l'hook quando non è più necessario.
Non possiamo pensare di applicare hook IAT poiché non vengono sempre chiamati e quindi sono inaffidabili. L'hooking inline è una tecnica potente; tuttavia richiede di patchare la memoria in cui risiede il codice. Questa è una tecnica potente, ma strumenti come PE-Sieve e Moneta possono distinguere la differenza tra la copia residente in memoria e quella su disco di un modulo e segnalarla. Questo ci lascia con lo strumento perfetto per il lavoro: i registri di debug, anche se sono piuttosto sottovalutati dagli autori di malware!
Su Windows, a livello generale, un processo è essenzialmente un incapsulamento di thread, e ciascuno di questi thread mantiene un contesto che è lo stato del thread: registri, stack, ecc. I registri di debug sono una risorsa privilegiata, e lo è anche impostarli; tuttavia, Windows espone varie syscall che ci consentono di richiedere che il kernel compia un'azione privilegiata per nostro conto; questo include l'impostazione dei registri di debug, che sono perfetti per noi. NtSetThreadContext e NtGetThreadContext espongono funzionalità per modificare qualsiasi contesto di thread per cui possiamo aprire un handle con il privilegio necessario. Possiamo vedere come impostare i registri di debug con l'API Win32.```c CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
// set our debug information in the Dr registers
SetThreadContext(thd, &context);
There are 8 Debug registers, from Dr0 through to Dr7. The ones of interest to us are only
Dr0-3 which we store addresses we would like to break on, and Dr6 is just the debug
status. Most importantly is Dr7, which describes the breakpoints conditions in which the
processor will throw an exception. There are various limitations when using debug
registers, such as a limited number (4) and not being applied to all threads/newly
spawned threads. We will look to address some of these limitations!
When the exception is thrown, it will look for an exception handler which we can define
and register in our program [1]. In our defined exception handler, we want our associated
code (different code flows) to run when the corresponding breakpoint is triggered.
Ci sono 8 registri di debug, da Dr0 a Dr7. Quelli di nostro interesse sono solo
Dr0-3, in cui memorizziamo gli indirizzi su cui vogliamo fermarci, e Dr6 è semplicemente lo stato di debug.
La cosa più importante è Dr7, che descrive le condizioni dei breakpoint in base alle quali il
processore lancerà un'eccezione. Ci sono varie limitazioni nell'uso dei registri di debug,
come un numero limitato (4) e la non applicazione a tutti i thread/thread appena generati.
Cercheremo di affrontare alcune di queste limitazioni!
Quando l'eccezione viene lanciata, il processore cercherà un gestore di eccezioni che possiamo definire
e registrare nel nostro programma [1]. Nel nostro gestore di eccezioni definito, vogliamo che il codice
associato (diversi flussi di codice) venga eseguito quando viene attivato il breakpoint corrispondente.```c
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
// Look for our associated code flow relative to our RIP
if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) {
HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
Questo è ottenuto da una funzione costruttrice che imposta la mappatura tra una "callback" funzione lambda e un indirizzo. using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;```c typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
// Global std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
// Create our mapping HWBP_ADDRESS_MAP[address].func = function; HWBP_ADDRESS_MAP[address].pos = pos;
Dobbiamo iterare attraverso tutti i thread del nostro processo e impostare le corrispondenti regolazioni al
contesto per essi. Questo può essere ottenuto utilizzando le funzioni di supporto ToolHelp32:
CreateToolhelp32Snapshot e Thread32Next. Non è nulla di elaborato, ma affronta uno dei
nostri limiti di non agganciarci a tutti i thread.```c
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true)
{
DWORD pid{ GetCurrentProcessId() };
HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if (h != INVALID_HANDLE_VALUE) {
THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) };
if (Thread32First(h, &te)) {
do {
if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
HANDLE thd = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
Avere dei breakpoint hardware impostati è potenzialmente sospetto, poiché potrebbero indicare attività dannose (anche se, per quanto ne so, nessun EDR li scansiona attivamente). Possono essere usati contro di noi come potenziale IoC, quindi dobbiamo rimuovere ogni loro traccia una volta terminato di usarli.
Possiamo implementare tutto questo nella nostra funzione deconstructor!! Questa itererà attraverso tutti i thread e controllerà se il registro (&context.Dr0)[pos] punta all'indirizzo in cui abbiamo inizialmente impostato il breakpoint hardware (pos è solo un indice % 4 che ci dà accesso a context.Dr0-Dr3). Possiamo anche rimuovere le condizioni necessarie nel registro Dr7. Dobbiamo anche ricordarci di rimuovere la nostra voce nella mappatura. Pertanto, il nostro breakpoint hardware sarà presente solo per la durata richiesta!```c SetHWBPS(address, pos, false); HWBP_ADDRESS_MAP.erase(address);
Un esempio di hardware breakpoint sarebbe Sleep, dove sostituiamo semplicemente la durata del sonno
con 0.```c
HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, // Set Dr 0
([&](PEXCEPTION_POINTERS ExceptionInfo) {
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
Sappiamo di impostare RCX a causa della convenzione di chiamata rapida a quattro registri di Windows x64[1]. Il primo argomento del costruttore è l'indirizzo su cui interrompere, il secondo è quale registro Dr0- 3 in cui memorizzare (nota: possiamo avere solo 4 indirizzi su cui interrompere alla volta), e il terzo è una funzione lambda che catturerà per riferimento PEXCEPTION_POINTERS, che è l'informazione che un gestore di eccezioni riceverà. Questo ci consentirà in definitiva di controllare il flusso di un programma in modo diverso a seconda di quale breakpoint è stato attivato.