枚举消息钩子是 ARK 的经典功能之一,从 ARK 的鼻祖 IceSword 开始就有了此项功能。接下来我就给大家解密是怎么实现枚举消息钩子的。


接下来进入正题,说说枚举消息钩子的总体思路。首先获得名为 gSharedInfo的全局变量的地址(此变量在 user32.dll 里被导出),它的值其实是一个内核结构体 win32k!tagsharedinfo 的地址:

lkd > dt win32k!tagsharedinfo
+ 0x000 psi : Ptr64 tagSERVERINFO
+ 0x008 aheList : Ptr64 _HANDLEENTRY
+ 0x010 HeEntrySize : Uint4B
+ 0x018 pDispInfo : Ptr64 tagDISPLAYINFO
+ 0x020 ulSharedDelta : Uint8B
+ 0x028 awmControl : [31] _WNDMSG
+ 0x218 DefWindowMsgs : _WNDMSG
+ 0x228 DefWindowSpecMsgs : _WNDMSG

接下来 tagSERVERINFO.cHandleEntries 的值,这个值记录了当前消息钩子的数目(记为 count):

lkd > dt win32k!tagSERVERINFO
+ 0x000 dwSRVIFlags : Uint4B
+ 0x008 cHandleEntries : Uint8B
+ 0x010 mpFnidPfn : [32] Ptr64 int64
+ 0x110 aStoCidPfn : [7] Ptr64 int64
[以下内容太长省略......]

然后读取 gSharedInfo+8 的值,获得 aheList 的值(记为 phe),此值为首个HANDLEENTRY 结构体的地址:

lkd > dt win32k!_HANDLEENTRY
+ 0x000 phead : Ptr64 _HEAD
+ 0x008 pOwner : Ptr64 Void
+ 0x010 bType : UChar
+ 0x011 bFlags : UChar
+ 0x012 wUniq : Uint2B

接下来,从 phe 首地址开始,获得【count-1】个的 HANDLEENTRY 结构体的指针, HANDLEENTRY.phead 记录的值指向一个 HEAD 结构体, HEAD 结构体才记录了每个消息钩子的具体信息。当然也不是每个 HANDLEENTRY 都是消息钩子,只有当HANDLEENTRY.bType 为 5 时才是消息钩子。 HEAD 结构体的定义如下(记录了不少有用的信息,比如钩子类型、钩子句柄、钩子函数地址等):

lkd > dt win32k!taghook
+ 0x000 head : _THRDESKHEAD
+ 0x028 phkNext : Ptr64 tagHOOK
+ 0x030 iHook : Int4B
+ 0x038 offPfn : Uint8B
+ 0x040 flags : Uint4B
+ 0x044 ihmod : Int4B
+ 0x048 ptiHooked : Ptr64 tagTHREADINFO
+ 0x050 rpdesk : Ptr64 tagDESKTOP
+ 0x058 nTimeout : Pos 0, 7 Bits
+ 0x058 fLastHookHung : Pos 7, 1 Bit

由于结构十分复杂,看得迷糊的人可以边看本文边用 WINDBG 进行内核调试。接下来,给出实现的代码(代码倒十分简短):

void EnumMsgHook()
{
    int i = 0;
    UINT64 pgSharedInfo = 0;
    pgSharedInfo = (UINT64)GetProcAddress(GetModuleHandleA("user32.dll"), "gSharedInfo");
    UINT64 phe = GetQWORD(pgSharedInfo + 8);
    UINT64 count = GetQWORD(GetQWORD(pgSharedInfo) + 8);
    HANDLEENTRY heStruct = { 0 };
    HOOK_INFO Hook = { 0 };
    for (i = 0; i < count; i++)
    {
        memcpy(&heStruct, (PVOID)(phe + i*sizeof(HANDLEENTRY)), sizeof(HANDLEENTRY));
        if (heStruct.bType == 5)
        {
            RKM(heStruct.phead, &Hook, sizeof(HOOK_INFO));
            printf("hHandle: 0x%llx\n", Hook.hHandle);
            printf("iHookFlags: %s\n", GetHookFlagString(Hook.iHookFlags));
            printf("iHookType: %s\n", GetHookType(Hook.iHookType));
            printf("OffPfn: 0x%llx\n", Hook.OffPfn);
            printf("ETHREAD: 0x%llx\n", GetQWORD((UINT64)(Hook.Win32Thread)));
            printf("ProcessName: %s\n\n", GetPNbyET(GetQWORD((UINT64)(Hook.Win32Thread))));
        }
    }
}

接下来解析子函数。首先是读取内核内存的函数,此函数可用于安全读写内核内存:

BOOLEAN VxkCopyMemory(PVOID pDestination, PVOID pSourceAddress, SIZE_T SizeOfCopy)
{
    PMDL pMdl = NULL;
    PVOID pSafeAddress = NULL;
    pMdl = IoAllocateMdl(pSourceAddress, (ULONG)SizeOfCopy, FALSE, FALSE, NULL);
    if (!pMdl) return FALSE;
    __try
    {
        MmProbeAndLockPages(pMdl, KernelMode, IoReadAccess);
    } _
        _except(EXCEPTION_EXECUTE_HANDLER)
    {
        IoFreeMdl(pMdl);
        return FALSE;
    }
    pSafeAddress = MmGetSystemAddressForMdlSafe(pMdl, NormalPagePriority);
    if (!pSafeAddress) return FALSE;
    RtlCopyMemory(pDestination, pSafeAddress, SizeOfCopy);
    MmUnlockPages(pMdl);
    IoFreeMdl(pMdl);
    return TRUE;
}

通过判断 phead->iHook 可以获得钩子的类型:

char *GetHookType(int Id)
{
    char *string;
    string = (char*)malloc(32);
    switch (Id)
    {
    case -1:
    {
        strcpy(string, "WH_MSGFILTER");
        break;
    }
    case 0:
    {
        strcpy(string, "WH_JOURNALRECORD");
        break;
    }
    case 1:
    {
        strcpy(string, "WH_JOURNALPLAYBACK");
        break;
    }
    case 2:
    {
        strcpy(string, "WH_KEYBOARD");
        break;
    }
    case 3:
    {
        strcpy(string, "WH_GETMESSAGE");
        break;
    }
    case 4:
    {
        strcpy(string, "WH_CALLWNDPROC");
        break;
    }
    case 5:
    {
        strcpy(string, "WH_CBT");
        break;
    }
    case 6:
    {
        strcpy(string, "WH_SYSMSGFILTER");
        break;
    }
    case 7:
    {
        strcpy(string, "WH_MOUSE");
        break;
    }
    case 8:
    {
        strcpy(string, "WH_HARDWARE");
        break;
    }
    case 9:
    {
        strcpy(string, "WH_DEBUG");
        break;
    }
    case 10:
    {
        strcpy(string, "WH_SHELL");
        break;
    }
    case 11:
    {
        strcpy(string, "WH_FOREGROUNDIDLE");
        break;
    }
    case 12:
    {
        strcpy(string, "WH_CALLWNDPROCRET");
        break;
    }
    case 13:
    {
        strcpy(string, "WH_KEYBOARD_LL");
        break;
    }
    case 14:
    {
        strcpy(string, "WH_MOUSE_LL");
        break;
    }
    default:
    {
        strcpy(string, "????");
        break;
    }
    }
    return string;
}

通过判断 phead->flags 可以获得钩子的标志。钩子标志的意思是,有些钩子只针对当前进程有效,有些钩子针对全局有效。比如 QQ 密码框的消息钩子就是全局钩子:

char *GetHookFlagString(int Flag)
{
    char *string;
    string = (char*)malloc(8);
    if (Flag == 1 || Flag == 3)
        strcpy(string, "Global");
    else
        strcpy(string, "Local");
    return string; 
}

打赏作者