通过hook资源管理器获取文本插入点坐标

2022.10.6 更新: 优化了代码, 增强了兼容性

目前没找到有什么泛用的方法获取文本插入点的坐标,GetCaretPos只适用于很小部分的标准控件,另一种方法是使用MSAA(ACC),这已经能够获取很多非标控件中的光标位置了,但对于UWP,WPF等窗口依然是无能为力. 网上查了很久也没头绪,无奈之下只能动用点非常手段了。

既然我做不到,那就找人要就好了,会使用Win+V剪贴板的朋友会发现,它即使在UWP,WPF应用中也能顺利找到我们的光标位置并显示剪贴板窗口在下方。遗憾的是,我并不知道它是怎么实现这一功能的,但起码我能借用一下它,微软应该不会介意吧。

通过hook资源管理器获取文本插入点坐标

通过WindowSpy我们可以找到这个剪贴板窗口的所属进程就是Explorer资源管理器,那么我们需要的数据很有可能就在这个进程里面。通过Cheat Engine确实找到了这个数据地址,是一个16字节的RECT结构,储存了光标左上和右下的坐标。或许可以寻找它的基址然后用ahk直接读取,但这样问题很明显:一旦windows版本变动,基址就很可能失效,维护非常困难,而且频繁地跨进程读取内存让人很不放心。我的目标是修改它的代码,让它在坐标数据变动的时候主动通知我们的ahk进程。

反汇编发现这个RECT结构是通过msvcrt.dll的memcpy函数修改的,所以我要做的就是hook掉这个函数,让它在修改RECT结构的时候用PostMessage顺便通知一下我的ahk进程。

大致的思路,需要一定的WINAPI调用经验和系统底层知识:

1.用ToolHelp函数获取explorer.exe的进程id,然后获取msvcrt.dll模块基址,加上调用GetProcAddress获取的memcpy函数的偏移就能确定需要hook的地址。

2.OpenProcess打开explorer的进程句柄,VirtualAllocEx分配内存,WriteProcessMemory写入通过汇编器生成的机器码。要求先执行call将要覆盖掉的原来的代码,保证不影响原来的功能,然后再确定是不是我们要的数据,例如RECT是16字节,所以memcpy的字节数参数必然是16。当然再观察一下还能发现更多的判断条件。当确定是我们需要的数据后就可以调用PostMessage发送数据到我们的ahk窗口了。

3.最后同样用WriteProcessMemory注入call到第一步找到的地址,这样每当它修改RECT结构后也会顺便也执行我们刚才第二步写好的代码。

4.做个文明人,ahk退出时将call覆盖的原来的代码重新用WriteProcessMemory写回去,并用VirtualFreeEx释放分配的内存。

大概就讲个思路,具体需要的汇编知识和注入知识就不多赘述了。

AHKV2.0-beta.6+的具体实现,Win10和Win11测试通过:

RegisterCaretMessageHook(flag) {
    static pShellCode := 0
    hProcess := hMsvcrt := 0

    if !pid := ProcessExist("explorer.exe")
        throw Error('"explorer.exe" not found')
    if !hProcess := DllCall("OpenProcess", "uint", 0x1fffff, "int", false, "uint", pid, "ptr")
        throw OSError()
    try {
        if !hMsvcrt := DllCall("LoadLibraryW", "str", "msvcrt.dll", "ptr")
            throw OSError()
        pLocalMemcpy := DllCall("GetProcAddress", "ptr", hMsvcrt, "astr", "memcpy", "ptr")
        if !msvcrtEntry := ToolHelpFindModuleByName("msvcrt.dll", pid)
            throw Error('"msvcrt.dll" not found')
        pMemcpy := msvcrtEntry.modBaseAddr + pLocalMemcpy - hMsvcrt
        msg := DllCall("RegisterWindowMessage", "str", "WM_CARETPOSCHANGED")

        if !flag || pShellCode {
            if !flag {
                OnExit(unregister, 0)
                DllCall("ChangeWindowMessageFilterEx", "ptr", A_ScriptHwnd, "uint", msg, "uint", 0, "ptr", 0)
            }
            if !DllCall("WriteProcessMemory", "ptr", hProcess, "ptr", pMemcpy, "ptr", pLocalMemcpy, "uptr", 16, "uptr*", 0)
                throw OSError()
            if pShellCode {
                DllCall("VirtualFreeEx", "ptr", hProcess, "ptr", pShellCode, "uptr", 0, "uint", 0x8000)
                pShellCode := 0
            }
            return
        }

        memcpyBytes := DllCall("GetProcAddress", "ptr", hMsvcrt, "astr", "memset", "ptr") - pLocalMemcpy

        if !user32Entry := ToolHelpFindModuleByName("user32.dll", pid)
            throw Error('"User32.dll" not found')
        hUser32 := DllCall("GetModuleHandle", "str", "user32", "ptr")
        pPostMessageW := user32Entry.modBaseAddr + DllCall("GetProcAddress", "ptr", hUser32, "astr", "PostMessageW", "ptr") - hUser32

        if !coreUiComponentsEntry := ToolHelpFindModuleByName("CoreUIComponents.dll", pid)
            throw Error('"CoreUIComponents.dll" not found')

        CryptHexStringToBinary("00000000000000000000000000000000000000000000000090909090909090904983F8100F85970000004C8B54240849BB00000000000000004D29DA4981FA000000000F8378000000837914090F840A000000837914070F856400000083791C000F855A0000004C8B124C8B5A084C3B158BFFFFFF0F850D0000004C3B1D86FFFFFF0F84390000004C891571FFFFFF4C891D72FFFFFF415052514883EC404D8BCB4D8BC2BA0000000048B90000000000000000FF1557FFFFFF4883C440595A4158", &shellCodeBuf)
        NumPut("ptr", pPostMessageW, shellCodeBuf, 0x10)
        NumPut("ptr", coreUiComponentsEntry.modBaseAddr, shellCodeBuf, 0x31)
        NumPut("uint", coreUiComponentsEntry.modBaseSize, shellCodeBuf, 0x3F)
        NumPut("uint", msg, shellCodeBuf, 0xA5)
        NumPut("ptr", A_ScriptHwnd, shellCodeBuf, 0xAB)

        if !pShellCode := DllCall("VirtualAllocEx", "ptr", hProcess, "ptr", 0, "uptr", shellCodeBuf.Size + memcpyBytes, "uint", 0x1000, "uint", 0x40, "ptr")
            throw OSError()
        CryptHexStringToBinary("48B80000000000000000FFD0C3", &hookBuf)
        NumPut("ptr", pShellCode + 32, hookBuf, 2)

        if !DllCall("WriteProcessMemory", "ptr", hProcess, "ptr", pShellCode, "ptr", shellCodeBuf, "uptr", shellCodeBuf.Size, "uptr*", 0)
            throw OSError()
        if !DllCall("WriteProcessMemory", "ptr", hProcess, "ptr", pShellCode + shellCodeBuf.Size, "ptr", pLocalMemcpy, "uptr", memcpyBytes, "uptr*", 0)
            throw OSError()
        if !DllCall("WriteProcessMemory", "ptr", hProcess, "ptr", pMemcpy, "ptr", hookBuf, "uptr", hookBuf.Size, "uptr*", 0)
            throw OSError()
        DllCall("ChangeWindowMessageFilterEx", "ptr", A_ScriptHwnd, "uint", msg, "uint", 1, "ptr", 0)
        OnExit(unregister)
    }
    catch as e {
        if pShellCode {
            DllCall("VirtualFreeEx", "ptr", hProcess, "ptr", pShellCode, "uptr", 0, "uint", 0x8000)
            pShellCode := 0
        }
        throw e
    }
    finally {
        if hMsvcrt
            DllCall("FreeLibrary", "ptr", hMsvcrt)
        DllCall("CloseHandle", "ptr", hProcess)
    }
    return msg

    static unregister(*) => RegisterCaretMessageHook(false)
}

/*
alloc(data, 2048)
label(begin)
label(ready)
label(memcpy)
label(L1)

data:
dq 0,0,0
dq 9090909090909090

begin:
cmp r8,10
jne memcpy
mov r10,[rsp+08]
mov r11,0000000000000000 // uicorecomponents address
sub r10,r11
cmp r10,00000000 // uicorecomponents size
jae memcpy
cmp dword ptr[rcx+14],9
je L1
cmp dword ptr[rcx+14],7
jne memcpy
L1:
cmp dword ptr[rcx+1C],0
jne memcpy
mov r10,[rdx]
mov r11,[rdx+08]
cmp r10,[data]
jne ready
cmp r11,[data+08]
je memcpy

ready:
mov [data],r10
mov [data+08],r11
push r8
push rdx
push rcx
sub rsp,40
mov r9,r11
mov r8,r10
mov edx,0 // msg
mov rcx,0 // hwnd
call [data+10]
add rsp,40
pop rcx
pop rdx
pop r8

memcpy:
*/

CryptHexStringToBinary(hexString, &binary){
    DllCall("crypt32\CryptStringToBinaryW", "str", hexString, "uint", len := StrLen(hexString), "uint", 4, "ptr", 0, "uint*", &bytes := 0, "ptr", 0, "ptr", 0)
    return DllCall("crypt32\CryptStringToBinaryW", "str", hexString, "uint", len, "uint", 4, "ptr", binary := binary ?? Buffer(bytes), "uint*", bytes, "ptr", 0, "ptr", 0)
}

ToolHelpFindModuleByName(moduleName, pid := 0) {
    if !snapshot := DllCall("CreateToolhelp32Snapshot", "uint", 0x18, "uint", pid, "ptr")
        return
    entry := tagMODULEENTRY32W()
    entry.dwSize := entry.Size
    next := DllCall("GetProcAddress", "Ptr", DllCall("GetModuleHandle", "str", "kernel32", "ptr"), "astr", "Module32NextW", "ptr")

    res := DllCall("Module32FirstW", "ptr", snapshot, "ptr", entry)
    while res {
        if entry.szModule = moduleName {
            res := entry
            break
        }
        res := DllCall(next, "ptr", snapshot, "ptr", entry)
    }
    DllCall("CloseHandle", "ptr", snapshot)
    return res
}

class tagMODULEENTRY32W {
    Size := 1080
    __New() => this.Ptr := (this._ := Buffer(this.Size)).Ptr
    dwSize {
        get => NumGet(this, "uint")
        set => NumPut("uint", Value, this)
    }
    th32ModuleID => NumGet(this, 4, "uint")
    th32ProcessID => NumGet(this, 8, "uint")
    GlblcntUsage => NumGet(this, 12, "uint")
    ProccntUsage => NumGet(this, 16, "uint")
    modBaseAddr => NumGet(this, 24, "ptr")
    modBaseSize => NumGet(this, 32, "uint")
    hModule => NumGet(this, 40, "ptr")
    szModule =>  StrGet(this.Ptr + 48)
    szExePath => StrGet(this.Ptr + 560)
}

使用例1 实时获取当前文本插入点坐标:

Persistent
OnMessage(RegisterCaretMessageHook(true), CaretMsgHandler1)

CaretMsgHandler1(leftTop, rightBottom, msg, hwnd){
    oldCoordMode := A_CoordModeToolTip
    A_CoordModeToolTip := "Screen"
    x := rightBottom & 0xffffffff
    y := rightBottom >> 32
    if x > 0x7fffffff
        x -= 0x100000000
    if y > 0x7fffffff
        y -= 0x100000000
    ToolTip x " | " y, x, y
    A_CoordModeToolTip := oldCoordMode
}

使用例2 当输入焦点切换的时候显示当前输入法中英状态:

Persistent
OnMessage(RegisterCaretMessageHook(true), CaretMsgHandler2)

CaretMsgHandler2(leftTop, rightBottom, msg, hwnd){
    static lastHasCaret := 0, showToolTip := 0

    hasCaret := leftTop || rightBottom
    if !lastHasCaret && hasCaret {
        showToolTip := 1
        SetTimer(StopShowToolTip, -2000)
    }
    else if lastHasCaret && !hasCaret {
        StopShowToolTip()
    }
    lastHasCaret := hasCaret

    if showToolTip && hasCaret {
        if rightBottom == leftTop + 1 {
            DllCall("SystemParametersInfo", "uint", 48, "uint", 0, "ptr", rect := Buffer(16), "uint", 0)
            if NumGet(rect, 8, "int64") == rightBottom + 0x100000000
                return 0
        }
        oldCoordMode := A_CoordModeToolTip
        A_CoordModeToolTip := "Screen"
        x := rightBottom & 0xffffffff
        y := rightBottom >> 32
        if x > 0x7fffffff
            x -= 0x100000000
        if y > 0x7fffffff
            y -= 0x100000000
        try
            ToolTip SendMessage(0x0283, 0x005, 0, DllCall("imm32\ImmGetDefaultIMEWnd", "ptr", WinExist("A"), "ptr")) ? "中" : "英", x, y
        A_CoordModeToolTip := oldCoordMode
    }
    return 0

    static StopShowToolTip() => (ToolTip(), showToolTip := 0)
}

效果,现在即使是设置这类界面也能获取光标坐标了:

通过hook资源管理器获取文本插入点坐标

给TA捐赠
共{{data.count}}人
人已捐赠
其他应用教程

scite个人强化修改总结

2022-10-1 19:03:40

教程

[AHK]面向对象练习-计算并输出过道和栅栏的造价

2023-1-6 10:08:56

7 条回复 A文章作者 M管理员
  1. ahker
    ahker给您捐赠了¥20
  2. hei

    看起来好厉害,学习学习

  3. hei
    hei给您捐赠了¥5
  4. dbgba
    dbgba给您捐赠了¥5
  5. 蜜獾哥
    蜜獾哥给您捐赠了¥5
  6. MSFT

    win11 25276下测试了例2,一直显示“中”,有点问题。例1没有问题。

  7. pxs

    大佬,我使用这段hook时遇到了问题,如果把输入法语言设置为“英语(美国)”,那么就不会生效,切换回中文输入法又重新生效了,请问这是什么原因?

个人中心
购物车
优惠劵
有新私信 私信列表
搜索