※ 참고 : 이 글은 "User32!SetWindowsHookExA/W 를 후킹하지 않는다" 와 "오직 커널 레벨에서만 막는다(?)"는 가정하에 작성되었습니다.
보통 메시지 훅을 할 때는, User32.dll에 Export 되어있는 SetWindowsHookExA/SetWindowsHookExW 라는 API를 이용합니다.
[물론 아닌 경우도 있겠지만, 적어도 저는 이걸 이용합니다!]
이 API를 이용해서 DLL을 Injection을 할 수도 있습니다.
아래와 같은 경우죠.
DLL InjectTarget.dll의 Export된 함수인 MsgProc를 HOOKPROC로 사용하는 경우
INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, INT nCmdShow)
{
PVOID hHook;
PVOID AddressOfFunction=(PVOID)GetProcAddress(LoadLibrary("injectTarget.dll"), "MsgProc");
hHook=(PVOID)SetWindowsHookEx(......, AddressOfFunction, ......);
......중략......
}
그런데, 이상한 것은, SetWindowsHookExA/SetWindowsHookExW는 다른 함수와는 달리, 프로세스에 별다른 접근을 하지 않고
바로 SYSENTER를 합니다.
그리하여, 프로세스를 숨겨버린다 해도 SetWindowsHookEx()를 막기는 힘듭니다.
심지어 자기자신을 숨기고, KeAttachProcess/KeStackAttachProcess마저 후킹하는 nProtect마저도
SetWindowsHookEx()를 이용한 DLL Injection은 막지 못하였습니다. [지금은 바뀌었는지 모르겠지만]
필자는 프로세스를 보호하기 위해 커널 모드에서 PsLookupXxx 루틴과 KeXxxAttachProcess 루틴, 심지어는 ObXxx 루틴까지 후킹을 했으나
윈도우 프로세스의 경우 SetWindowsHookEx를 막아낼 수 없었습니다.
그 이유를 지금부터 알아보기 위해, SetWindowsHookExA/W를 차차 살펴보기로 합시다.
USER32!SetWindowsHookExA를 보면......
USER32!SetWindowsHookExW를 보면......
위와 같이, USER32.DLL의 SetWindowsHookA/W는 내부적으로 USER32.DLL::SetWindowsHookExAW를 호출합니다.
이제는 SetWindowsHookExAW를 보면......
SetWindowsHookAW는 USER32!_SetWindowsHookEx를 호출하는군요.
이어서 _SetWindowsHookEx를 봅시다.
_SetWindowsHookEx는 결과적으로 USER32!NtUserSetWindowsHookEx를 호출합니다.
아마도 USER32!NtUserSetWindowsHookEx는 NTDLL!NtXxx 루틴과 비슷한 일을 하는것으로 예상됩나다.
드디어, NtUserSetWindowsHookEx를 보는군요.
자 이것을 전부 정리(?) 해 보면,
SetWindowsHookExA/SetWindowsHookExW->SetWindowsHookExAW->_SetWindowsHookEx->NtUserSetWindowsHookEx 순이 되는군요.
그렇다면, Win32K!NtUserSetWindowsHookEx를 확인해 봐야겠군요.
자, Win32K!NtUserSetWindowsHookEx를 봅시다.
자, 보이십니까???
Win32K!NtUserSetWindowsHookEx는 내부적으로 Internal 함수인 Win32k!zzzSetWindowsHookEx를 호출합니다.
Win32k!zzzSetWindowsHookEx를 한번 보면......
여기서는 별다른 특별한 것은 보이지 않았습니다.
다음 장을 봅시다.
으으으음. AddHmodDependency()라...... 웬지 이 함수를 보면 뭔가 알 수 있을 것 같기도 한데.......
계속해서 봅시다.
으음.....zzzJournalAttach같은 내부함수도 보이는군요.
그나저나, 이 함수는 어디서 끝나는 걸까요.....하악하악
드디어 끝났습니다! [zzzSetWindowsHookEx가 이리 길줄이야......OTL]
으음....수많은 내부함수들이 보이는군요.
그나저나, 코드가 상당히 길어 분석이 좀 까다롭게 되었군요.
어쩔 수 없이, W2K 소스 참조.
PHOOK zzzSetWindowsHookEx(
HANDLE hmod,
PUNICODE_STRING pstrLib,
PTHREADINFO ptiThread,
int nFilterType,
PROC pfnFilterProc,
DWORD dwFlags)
{
ACCESS_MASK amDesired;
PHOOK phkNew;
TL tlphkNew;
PHOOK *pphkStart;
PTHREADINFO ptiCurrent;
/*
* Check to see if filter type is valid.
*/
if ((nFilterType < WH_MIN) || (nFilterType > WH_MAX)) {
RIPERR0(ERROR_INVALID_HOOK_FILTER, RIP_VERBOSE, "");
return NULL;
}
/*
* Check to see if filter proc is valid.
*/
if (pfnFilterProc == NULL) {
RIPERR0(ERROR_INVALID_FILTER_PROC, RIP_VERBOSE, "");
return NULL;
}
ptiCurrent = PtiCurrent();
if (ptiThread == NULL) {
/*
* Is the app trying to set a global hook without a library?
* If so return an error.
*/
if (hmod == NULL) {
RIPERR0(ERROR_HOOK_NEEDS_HMOD, RIP_VERBOSE, "");
return NULL;
}
} else {
/*
* Is the app trying to set a local hook that is global-only?
* If so return an error.
*/
if (!(abHookFlags[nFilterType + 1] & HKF_TASK)) {
RIPERR0(ERROR_GLOBAL_ONLY_HOOK, RIP_VERBOSE, "");
return NULL;
}
/*
* Can't hook outside our own desktop.
*/
if (ptiThread->rpdesk != ptiCurrent->rpdesk) {
RIPERR0(ERROR_ACCESS_DENIED,
RIP_WARNING,
"Access denied to desktop in zzzSetWindowsHookEx - can't hook other desktops");
return NULL;
}
if (ptiCurrent->ppi != ptiThread->ppi) {
/*
* Is the app trying to set hook in another process without a library?
* If so return an error.
*/
if (hmod == NULL) {
RIPERR0(ERROR_HOOK_NEEDS_HMOD, RIP_VERBOSE, "");
return NULL;
}
/*
* Is the app hooking another user without access?
* If so return an error. Note that this check is done
* for global hooks every time the hook is called.
*/
if ((!RtlEqualLuid(&ptiThread->ppi->luidSession,
&ptiCurrent->ppi->luidSession)) &&
!(ptiThread->TIF_flags & TIF_ALLOWOTHERACCOUNTHOOK)) {
RIPERR0(ERROR_ACCESS_DENIED,
RIP_WARNING,
"Access denied to other user in zzzSetWindowsHookEx");
return NULL;
}
if ((ptiThread->TIF_flags & (TIF_CSRSSTHREAD | TIF_SYSTEMTHREAD)) &&
!(abHookFlags[nFilterType + 1] & HKF_INTERSENDABLE)) {
/*
* Can't hook console or GUI system thread if inter-thread
* calling isn't implemented for this hook type.
*/
RIPERR1(ERROR_HOOK_TYPE_NOT_ALLOWED,
RIP_WARNING,
"nFilterType (%ld) not allowed in zzzSetWindowsHookEx",
nFilterType);
return NULL;
}
}
}
/*
* Check if this thread has access to hook its desktop.
*/
switch( nFilterType ) {
case WH_JOURNALRECORD:
amDesired = DESKTOP_JOURNALRECORD;
break;
case WH_JOURNALPLAYBACK:
amDesired = DESKTOP_JOURNALPLAYBACK;
break;
default:
amDesired = DESKTOP_HOOKCONTROL;
break;
}
if (!RtlAreAllAccessesGranted(ptiCurrent->amdesk, amDesired)) {
RIPERR0(ERROR_ACCESS_DENIED,
RIP_WARNING,
"Access denied to desktop in zzzSetWindowsHookEx");
return NULL;
}
if (amDesired != DESKTOP_HOOKCONTROL &&
(ptiCurrent->rpdesk->rpwinstaParent->dwWSF_Flags & WSF_NOIO)) {
RIPERR0(ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION,
RIP_WARNING,
"Journal hooks invalid on a desktop belonging to a non-interactive WindowStation.");
return NULL;
}
#if 0
/*
* Is this a journal hook?
*/
if (abHookFlags[nFilterType + 1] & HKF_JOURNAL) {
/*
* Is a journal hook of this type already installed?
* If so it's an error.
* If this code is enabled, use PhkFirstGlobalValid instead
* of checking phkStart directly
*/
if (ptiCurrent->pDeskInfo->asphkStart[nFilterType + 1] != NULL) {
RIPERR0(ERROR_JOURNAL_HOOK_SET, RIP_VERBOSE, "");
return NULL;
}
}
#endif
/*
* Allocate the new HOOK structure.
*/
phkNew = (PHOOK)HMAllocObject(ptiCurrent, ptiCurrent->rpdesk,
TYPE_HOOK, sizeof(HOOK));
if (phkNew == NULL) {
return NULL;
}
/*
* If a DLL is required for this hook, register the library with
* the library management routines so we can assure it's loaded
* into all the processes necessary.
*/
phkNew->ihmod = -1;
if (hmod != NULL) {
#if defined(WX86)
phkNew->flags |= (dwFlags & HF_WX86KNOWNDLL);
#endif
phkNew->ihmod = GetHmodTableIndex(pstrLib);
if (phkNew->ihmod == -1) {
RIPERR0(ERROR_MOD_NOT_FOUND, RIP_VERBOSE, "");
HMFreeObject((PVOID)phkNew);
return NULL;
}
/*
* Add a dependency on this module - meaning, increment a count
* that simply counts the number of hooks set into this module.
*/
if (phkNew->ihmod >= 0) {
AddHmodDependency(phkNew->ihmod);
}
}
/*
* Depending on whether we're setting a global or local hook,
* get the start of the appropriate linked-list of HOOKs. Also
* set the HF_GLOBAL flag if it's a global hook.
*/
if (ptiThread != NULL) {
pphkStart = &ptiThread->aphkStart[nFilterType + 1];
/*
* Set the WHF_* in the THREADINFO so we know it's hooked.
*/
ptiThread->fsHooks |= WHF_FROM_WH(nFilterType);
/*
* Set the flags in the thread's TEB
*/
if (ptiThread->pClientInfo) {
BOOL fAttached;
/*
* If the thread being hooked is in another process, attach
* to that process so that we can access its ClientInfo.
*/
if (ptiThread->ppi != ptiCurrent->ppi) {
KeAttachProcess(&ptiThread->ppi->Process->Pcb);
fAttached = TRUE;
} else
fAttached = FALSE;
ptiThread->pClientInfo->fsHooks = ptiThread->fsHooks;
if (fAttached)
KeDetachProcess();
}
/*
* Remember which thread we're hooking.
*/
phkNew->ptiHooked = ptiThread;
} else {
pphkStart = &ptiCurrent->pDeskInfo->aphkStart[nFilterType + 1];
phkNew->flags |= HF_GLOBAL;
/*
* Set the WHF_* in the SERVERINFO so we know it's hooked.
*/
ptiCurrent->pDeskInfo->fsHooks |= WHF_FROM_WH(nFilterType);
phkNew->ptiHooked = NULL;
}
/*
* Does the hook function expect ANSI or Unicode text?
*/
phkNew->flags |= (dwFlags & HF_ANSI);
/*
* Initialize the HOOK structure. Unreferenced parameters are assumed
* to be initialized to zero by LocalAlloc().
*/
phkNew->iHook = nFilterType;
/*
* Libraries are loaded at different linear addresses in different
* process contexts. For this reason, we need to convert the filter
* proc address into an offset while setting the hook, and then convert
* it back to a real per-process function pointer when calling a
* hook. Do this by subtracting the 'hmod' (which is a pointer to the
* linear and contiguous .exe header) from the function index.
*/
phkNew->offPfn = ((ULONG_PTR)pfnFilterProc) - ((ULONG_PTR)hmod);
#ifdef HOOKBATCH
phkNew->cEventMessages = 0;
phkNew->iCurrentEvent = 0;
phkNew->CacheTimeOut = 0;
phkNew->aEventCache = NULL;
#endif //HOOKBATCH
/*
* Link this hook into the front of the hook-list.
*/
phkNew->phkNext = *pphkStart;
*pphkStart = phkNew;
/*
* If this is a journal hook, setup synchronized input processing
* AFTER we set the hook - so this synchronization can be cancelled
* with control-esc.
*/
if (abHookFlags[nFilterType + 1] & HKF_JOURNAL) {
/*
* Attach everyone to us so journal-hook processing
* will be synchronized.
* No need to DeferWinEventNotify() here, since we lock phkNew.
*/
ThreadLockAlwaysWithPti(ptiCurrent, phkNew, &tlphkNew);
if (!zzzJournalAttach(ptiCurrent, TRUE)) {
RIPMSG1(RIP_WARNING, "zzzJournalAttach failed, so abort hook %#p", phkNew);
if (ThreadUnlock(&tlphkNew) != NULL) {
zzzUnhookWindowsHookEx(phkNew);
}
return NULL;
}
if ((phkNew = ThreadUnlock(&tlphkNew)) == NULL) {
return NULL;
}
}
UserAssert(phkNew != NULL);
/*
* Later 5.0 GerardoB: The old code just to check this but
* I think it's some left over stuff from server side days.
.* Let's assert on it for a while
* Also, I added the assertions in the else's below because I reorganized
* the code and want to make sure we don't change behavior
*/
UserAssert(ptiCurrent->pEThread && THREAD_TO_PROCESS(ptiCurrent->pEThread));
/*
* Can't allow a process that has set a global hook that works
* on server-side winprocs to run at background priority! Bump
* up it's dynamic priority and mark it so it doesn't get reset.
*/
if ((phkNew->flags & HF_GLOBAL) &&
(abHookFlags[nFilterType + 1] & HKF_INTERSENDABLE)) {
ptiCurrent->TIF_flags |= TIF_GLOBALHOOKER;
KeSetPriorityThread(&ptiCurrent->pEThread->Tcb, LOW_REALTIME_PRIORITY-2);
if (abHookFlags[nFilterType + 1] & HKF_JOURNAL) {
ThreadLockAlwaysWithPti(ptiCurrent, phkNew, &tlphkNew);
/*
* If we're changing the journal hooks, jiggle the mouse.
* This way the first event will always be a mouse move, which
* will ensure that the cursor is set properly.
*/
zzzSetFMouseMoved();
phkNew = ThreadUnlock(&tlphkNew);
/*
* If setting a journal playback hook, this process is the input
* provider. This gives it the right to call SetForegroundWindow
*/
if (nFilterType == WH_JOURNALPLAYBACK) {
gppiInputProvider = ptiCurrent->ppi;
}
} else {
UserAssert(nFilterType != WH_JOURNALPLAYBACK);
}
} else {
UserAssert(!(abHookFlags[nFilterType + 1] & HKF_JOURNAL));
UserAssert(nFilterType != WH_JOURNALPLAYBACK);
}
/*
* Return pointer to our internal hook structure so we know
* which hook to call next in CallNextHookEx().
*/
DbgValidateHooks(phkNew, phkNew->iHook);
return phkNew;
}
음......단순한 구조체 조작만 하는 것 같지는 않아보이고
AddHmodDependency(), HMAllocObject(), ... 등이 뭔가를 하는 것 같은데 말이죠.
음 어쨌거나, SetWindowsHookEx()는 유저 모드에서는 물론, 커널 모드에서도 객체를 직접 수정하거나 Win32K의 또다른 내부함수를 호출하는 것이었기 때문에
SetWindowsHookEx()를 막을 수 없었던 것 같습니다.
HOOK 구조체를 초기화해도, 이미 Inject된 DLL이 언로드될것 같지는 않고 말이죠.
또한, 후킹되었다면 WIN32K!zzzUnhookWindowsHookEx()를 호출하는 방법도 있겠지만, 이 방법은 일단
zzzUnhookWindowsHookEx의 주소를 얻어와야 하므로, 굉장히 어렵습니다.
지금으로서는 Win32K!NtUserSetWindowsHookEx를 후킹하거나, Win32K!zzzSetWindowsHookEx를 후킹하는 수밖에 없는 것 같습니다.
(UserMode Hook은 제외)
Win32K!NtUserSetWindowsHookEx에서 Win32K!zzzSetWindowsHookEx를 호출하므로, 주소를 얻기는 쉽습니다.
실제로 제가 Win32K!zzzSetWindowsHookEx를 후킹해서 쓰고 있으나, 아직까지 별 문제는 일어나지 않았습니다.