Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
도구/GitHubGitHub/rad9800/hwbp4mw
IDS/IPS EvasionDebuggersPost-ExploitationRed TeamingAdversarial Attack
GitHubrad9800/hwbp4mw

hwbp4mw

디버그 레지스터를 사용하여 함수를 후킹하고 ETW/AMSI를 우회하며 사용자 영역 EDR 모니터링을 회피하는 Windows용 하드웨어 브레이크포인트 후킹 엔진입니다.

저장소 보기
27354183년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

이 글은 원래 VX-Underground Black Mass Halloween Edition 2022를 위한 것입니다.

후킹 엔진:

  • 멀티스레드 안전 x86/x64 hwbp 후킹 엔진 c
  • PAGE_GUARD/hwbp 브레이크포인트 라이브러리 c++20
  • hwbp 라이브러리 (DLL 예제) c++20

디버그 레지스터를 활용하는 범용 x64 사용자 영역 회피 기법:

  • TamperingSyscalls2 c
  • TamperingSyscalls2 c++20

사용 가능한 ETW/AMSI 후킹 예제

  • rad9800/misc

멀웨어를 위한 하드웨어 브레이크포인트 v 1.0

우리의 임무는 함수를 손쉽게 후킹하고 필요에 따라 코드 흐름을 우회시킨 다음, 마지막으로 더 이상 필요하지 않게 되면 후크를 제거하는 것입니다.

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 명령을 실행하지 않고 반환할 수 있도록 해야 합니다.
도구 다운로드