平常常用的三环Hook通常是InlineHook和虚Hook,这两种Hook方式均是通过修改内存来劫持控制流,InlineHook通过修改代码段上的代码而虚Hook通过修改虚表指针。因为均对内存做了修改(且往往Hook的位置都是敏感内存),很容易被诸如CRC之类的手段检测到。故学习了一下三环下的无痕HOOK,注意这里的无痕指的仅仅是不对代码段等内存产生修改,而不是不会被发现。
在x86架构下有一组特殊的寄存器叫做硬件断点寄存器,分别是Dr0-Dr7。其中Dr0-Dr3这四个寄存器用于于设置硬件断点的,Dr4和Dr5由系统保留,Dr6用于显示哪个硬件调试寄存器引发的断点,Dr7则是用于控制断点属性。如果我们能够设置硬件断点到我们想Hook的位置,那样程序运行到该处时就会抛出一个异常,转而进入异常处理函数;如果我们设置了与之匹配的异常处理函数,则可以在异常发生时劫持得到程序的控制流程流。 硬件断点寄存器可以通过修改线程上下文结构体(ThreadContext)来设置,而异常处理函数通常则是使用VEH。
下面来看一个demo,这个Demo实现了硬断MessageBoxA这个API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
HMODULE hUser32 = GetModuleHandleA("user32.dll");
size_t hookaddr = (size_t)GetProcAddress(hUser32, "MessageBoxA");
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)ExceptionHandler);
if(SetHwBreakpoint() == FALSE) printf("SetHwBreakpoint Error:%d\n", GetLastError());
break;
}
case DLL_THREAD_ATTACH: // 对每一个新创建的线程均添加硬件断点
{
SetHwBreakpoint();
}
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
BOOL SetHwBreakpoint()
{
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(GetCurrentThread(), &ctx);
ctx.Dr0 = hookaddr;
ctx.Dr7 = 0x1;
return SetThreadContext(GetCurrentThread(), &ctx);
}
size_t NTAPI ExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
if ((size_t)ExceptionInfo->ExceptionRecord->ExceptionAddress == hookaddr)
{
printf("Hook!\n");
return EXCEPTION_CONTINUE_EXECUTION;
}
else
{
//在异常handler里重设drx防止断点被意外清除
ExceptionInfo->ContextRecord->Dr0 = hookaddr;
ExceptionInfo->ContextRecord->Dr7 = 0x405;
return EXCEPTION_CONTINUE_SEARCH;
}
}
|
如果用心的去试一下,就会发现这个Demo在某些情况下是无法实现Hook的,比如在远线程注入该Dll的时候就发现Hook不住MessageboxA
为什么会这样呢?这里又是一个点,就是一个程序的上下文(寄存器等信息)是绑定于线程还是进程的。三环的远线程注入DLL本质是将Dll写入该进程的内存中,然后CreateRemoteThread在DllMain的位置创建了一个线程运行Dll的Main函数。DllMain函数中调用了SetHwBreakpoint对硬件断点寄存器做了修改,但此时这一条线程并不是程序原先运行代码的线程(主线程),而是我们远程创建的新线程,故设置出来的硬件断点仅对这条用于初始化dll的新线程有效。 正确的做法应该是枚举线程,对每一条线程均设置上硬件断点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
// DebugBreak();
printf("hUser32:%p\n", hUser32);
printf("hook:%p\n", hookaddr);
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)ExceptionHandler);
SetHBToAllThread(); // 为所有线程设置硬件断点
break;
}
case DLL_THREAD_ATTACH:
{
// 为新创建的线程设置硬件断点
SetHwBreakpoint(GetCurrentThread());
}
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
void SetHwBreakpoint(HANDLE hThread)
{
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(hThread, &ctx);
ctx.Dr0 = hookaddr;
ctx.Dr7 = 0x1;
if (SetThreadContext(hThread, &ctx) == FALSE)
printf("SetHwBreakpoint Error:%d\n", GetLastError());
}
// 枚举线程,并对每一条已存在的线程设置硬件断点
void SetHBToAllThread() {
HANDLE hThreadShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId());
THREADENTRY32* threadInfo = new THREADENTRY32;
HANDLE hThread = NULL;
threadInfo->dwSize = sizeof(THREADENTRY32);
int cnt = 0;
while (Thread32Next(hThreadShot, threadInfo) != FALSE)
{
if (GetCurrentProcessId() == threadInfo->th32OwnerProcessID)
{
cnt++;
printf("ThreadId:%x\n", threadInfo->th32ThreadID);
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadInfo->th32ThreadID);
SetHwBreakpoint(hThread);
CloseHandle(hThread);
}
}
CloseHandle(hThreadShot);
}
|