手把手逆向分析系统软键盘自动弹出逻辑--系统输入状态检测

引入话题

      在工作当中,用户需要一个功能,手机远程windows云桌面想进行输入时候,用户并不想使用windows内部的输入法进行用户输入,而是想使用手机自带的输入法进行输入,这比较符合用户的手机使用习惯;那做这个功能的话就需要去检测windows上的用户输入态,并检测到用户输入态之后通知手机唤起手机系统软键盘。这个功能其实有点类似windows系统在平板模式下,在用户输入状态的时候windows自动唤起系统软键盘。而用户想要的效果就是windows上的软键盘弹出效果。

 

分析TabTip.exe

        其实windows系统上的软键盘程序就是TabTip程序(windows server 2016以后系统版本都是这个进程),可以通过系统设置让系统显示软键盘托盘图标,点击图标就可以唤起这个进程。

这个进程会监听系统的输入状态,在设置了系统平板模式下,并使用触控屏,在输入状态下,会自动弹出软键盘;在非触控屏下,即使输入状态下,软键盘也不会自动弹出。如果想做到它的这种效果就需要看看TabTip到底做了什么,这就需要对TabTip进程进行逆向分析了。

分析猜想

       首先我猜测是存在一个类似如WM_IME_SETCONTEXT这个输入消息的系统消息,而只要监听这个系统消息就可以了;而这个消息到底是哪个消息呢,而需要知道是哪个消息就要分析TabTip了。

     注入插件到TabTip.exe进程,通过hook GetMessageW来监听TabTip到底监听了什么。

     这里发现TabTip监听了0x41F、0x413、0x411这类的消息,每次系统切换到输入框或者输入状态取消的时候,就会触发到这些消息,而如果将这些消息进行屏蔽的话,系统软键盘界面就不会自动弹出。说明这些消息应该就是我们需要监听的消息了,而这些消息到底是什么消息呢,通过网络搜索并没有找到这些消息的相关有价值信息,而我突然明白这些消息其实是用户自定义消息,因为WM_USER的值是0x400,而0x41F其实就是WM_USER+1F。通过中断线程并用windbg attach到TabTip进程,发现消息处理例程的模块其实是在tipskins.dll这个模块,在windbg查看堆栈,并在IDA跳转到消息处理例程函数的伪码:

而0x41F这类的自定义消息到底是哪里发送过来的呢,再次hook PostMessage这个api:

发现确实可以hook到类似如0x41F这类的消息,如果将这类消息屏蔽,你们系统软键盘自动功能就失效了,那就要看看到底是哪个函数调用的postmessage了。通过windbg查看堆栈调用情况:

tipskins其实是有符号的,只要系统环境变量设置了服务器符号宏路径,windbg或者IDA都会在自动下载到本地,堆栈里看到S_WnfCallback这个回调函数,而这个函数肯定是注册的系统回调,当有输入事件来的时候就会触发到这个回调,在IDA里面查看这个函数的引用情况:

 跳转到对应的调用,在CNotificationHandler::Initialize函数中发现了相关的调用:

这里发现了两个api:RtlQueryWnfStateData和RtlSubscribeWnfStateChangeNotification,而这两个函数的用法网络上资料并不多见,只发现一篇帖子有较大参考性(另一篇):Can I listen for option change events in the windows setting interface through C++? - Microsoft Q&A

     但他这个用法是用来监听microphone的接入事件的,并不符合我所要求的,但是这篇帖子大致告诉了我这个函数的调用方法,通过ida调试tabtip获取函数的传参并将上述代码的参数后发现回调函数就可以正常监听到系统输入事件了,经过对比回调函数的buffer参数发现传入的buffer内存内容是一致的。目前来说这就算是一个较大突破了。

 遇到困难

     突然发现调用RtlSubscribeWnfStateChangeNotification这个api之后,并设置了回调函数,在输入事件到来的时候会回调到我的回调函数,但是一旦退出/强杀TabTip.exe之后,监听回调就再也不会接收到任何调用了;出现这种问题我猜测有两种较大的可能性:1.我调用这个api的方式不对,比如函数传参传的有问题  2.TabTip一直会不断对系统进行“反馈”,在不断“反馈”的情况下,系统才会不断把系统输入状态派发给注册回调函数,因为看看模块的导入函数,也发现不少的rtl函数,会不会TabTip调用了如RtlPublishWnfStateData这类的函数,但是经过hook,好像该函数并没有被调用过。

      经过分析,我也排除了第二种TabTip不断“反馈”的可能性,因为我用windbg将TabTip Attach之后,我的回调函数还是可以正常接收到回调,在Attach的时候TabTip是被Freeze的,并不会进行“反馈”;

      同时发现在启动TabTip的时候,TabTip会反复调用RtlSubscribeWnfStateChangeNotification和RtlQueryWnfStateData,次数多大三四十次之多,而这是不是某一次传入了“正确”参数并调用该api之后,监听回调才正常工作呢。于是我在每次调用完RtlSubscribeWnfStateChangeNotification之后都进行中断,外部控制一步一步让程序进行运行:

经过一步一步运行之后发现在15次调用之后,等下一次调用该函数之后,回调函数就可以接收到回调了,于是我猜测这次传参才是正确的参数。 

那哪里调用过来的,这里用windbg查看堆栈,而为啥发现有这么多次调用该api呢,通过windbg看堆栈可以看到不同模块对这个api都有调用:

 在第15次关键调用的时候在tipskin.dll模块上打上断点:

但没有断下来,应该是在其他地方调用的,看看堆栈:

是CoreMessaging.dll模块调用了RtlSubscribeWnfStateChangeNotification,用ida分析这个dll:

堆栈上看上层是通过tipskins.dll的RemoteTabTipPolicy::RuntimeClassInitialize函数调用了CoreMessaging的CoreUICreate的接口然后顺序调用过来的。

 

而CoreMessaging模块到底是什么,百度一下,发现发现这个模块可能是一个系统服务:

在服务列表中发现:

   但是经过单步调试,发现并不是CoreMessaging模块调用完RtlSubscribeWnfStateChangeNotification之后,我注册的回调函数就可以收到回调了,而是后面还有一系列的函数调用,这跟我之前的预想不一样,这里就比较麻烦了,调试逻辑比较多比较麻烦,但是如果想规避可以通过直接唤起TabTip进程来规避这个问题(曲线救国)。

怎么无界面显示传参:

但是每次启动TabTip的时候,软键盘界面都会显示出来,看看系统传参:

在TabTip.exe的伪码里面找到CTabTipModule::ParseCommandLine:

Cmd里面执行: Tabtip.exe  /WinRE /ForceImmersive  就可以实现TabTip界面不显示同时回调又能收到的效果。

其实这个通知来着通知中心((164条消息) Windows 10中的窗体Z序_twinui.pcshell.dll_ALTaleX的博客-CSDN博客):

     

 新的发现

      我在procexp上看到TabTip有一个名字叫1ImmersiveFocusTrackingActiveEvent的Event,而如果将这个Event关闭的话,我注册的系统输入监听回调就接收不到回调了;于是我猜想肯定程序在另一个地方在等待这个Event,如果Event被关闭,程序就会调用一个卸载系统输入回调监听函数。

    好了,我再一次请出Apimonitor来hook TabTip对CreateEvent函数的调用:

 

这里我用Apimonitor抓取到了创建这个事件的调用过程,并通过调用堆栈看到这个函数是从tipskins.dll+0xf79c调用过来的,IDA上跳转到该偏移(模块基地址0x7FFAB90E0000+0xf79c): 

 

F5切换到伪代码:

 但是在资源管理器没找到该event的更多引用:

在Apimonitor里面Hook WaitForSingleObject、WaitForSingleObjectEx、WaitForMultipleObjects和WaitForMultipleObjectsEx并下调用后断点:

 但发现并没有触发到;

同样搜索wait Event信号的句柄值(0x00000000000047c),也发现没找到:

 现在怀疑这个Event不是本进程进行等待的,可能是外部程序进行等待的事件。

 为了验证猜想,我hook CreateEvent把Event的名称变成"lzlmersiveFocusTrackingActiveEvent":

这个时候发现,并回调函数并没有调过来,回调失效了,那说明这个Event的名称很重要,不能随便修改,因为这个名称可能是外部程序共同商定的名称;而且发现用windbg附加到Tabtip进程后中断进程执行同事关闭该Event的句柄,关闭之后回调消失,说明应该是其他进程在监测着这个事件。

经过调试发现,经过setevent之后回调就正常接收了:

经过尝试发现,只要创建了这个事件并设置了事件,系统就会有回调过来:

SECURITY_DESCRIPTOR secutityDesc;
::InitializeSecurityDescriptor(&secutityDesc, SECURITY_DESCRIPTOR_REVISION);
::SetSecurityDescriptorDacl(&secutityDesc, TRUE, NULL, FALSE);
SECURITY_ATTRIBUTES securityAttr;
securityAttr.nLength = sizeof SECURITY_ATTRIBUTES;
securityAttr.bInheritHandle = FALSE;
securityAttr.lpSecurityDescriptor = &secutityDesc;
HANDLE hImmersiveEvent = CreateEventW(&securityAttr, TRUE, FALSE, ImmersiveFocusEventName);
if (hImmersiveEvent)
{
    SetEvent(hImmersiveEvent);
}

而“1ImmersiveFocusTrackingActiveEvent”事件名称是怎么来的呢,通过调试发现在函数:void __fastcall CIPTipWnd::CreateFrame(CIPTipWnd *this)的1770行:

 v174 = (_WORD *)*((_QWORD *)this + 356);
发现v174取出来的值就是“1ImmersiveFocusTrackingActiveEvent”字符串,这个值应该就是保存在CIPTipWnd的一个成员变量,而这个变量在哪里赋值的呢,我调到类的构造函数:CIPTipWnd *__fastcall CIPTipWnd::CIPTipWnd(void):

 即这个event的名称是一个字符串常量,并不是外面传递进来的(前面的1为当前的sessionid)。

解析回调Buffer

通过调试s_WnfCallback回调函数,发现会调用到_OnImmersiveFocusTracking函数,将代码的伪码转到自己的代码里面,但是发现s_WnfCallback之后调用的函数不一定是_OnImmersiveFocusTracking,还有可能是_OnTouchEvent,在windbg里面对postmessage下断点,断下之后看堆栈:

通过打印日志发现s_WnfCallback参数里面_WNF_STATE_NAME为0x0F840539A3BC2035的时候会调用_OnTouchEvent,而_WNF_STATE_NAME为0x0F840539A3BC1835的时候调用_OnImmersiveFocusTracking:

 其实键盘输入界面的真正进程是TextInputHost.exe:

在抓取postmessage里面发现_OnTouchEvent里面post的是一个0x413的Msg,但同时也发现另一个线程post了一个0x41E的Msg:

 通过在windbg下断点并且打印堆栈:bp USER32!PostMessageW "k; .echo 'breaks'; g":

看到post 0x41E msg的堆栈信息,这里应该是窗口焦点切换的处理。

初步实现

经过对buffer的解析,发现只要判断 s_UnpackImmersiveFocusTracking函数里面解析的buffer数据就可以判定当前是否处于编辑状态了:

void s_UnpackImmersiveFocusTracking(PVOID a1, bool *a2, bool *a3, bool *a4, bool *a5,
    bool *a6, HWND *a7, HWND *a8, unsigned int *a9, RECT& a10, bool *a11, bool *a12, bool *a13)
{

.................................................................................  //  解析buffer

 
    if (*a3) // 触屏模式
    {
        if (*a2)
        {  // 处于编辑状态
            if (a10.left < a10.right && a10.top < a10.bottom)  // rect valid
            {
                ::PostMessage(g_hwnd, WM_SHOWINPUTWND, 0, 0);
            }
        }
        else
        {
            ::PostMessage(g_hwnd, WM_HIDEINPUTWND, 0, 0);
        }
    }
    else
    {  // 非触屏
        if (*a7 && *a8)  // HWND valid
        {
            if (*a2)
            {  // 处于编辑状态
                ::PostMessage(g_hwnd, WM_SHOWINPUTWND, 0, 0);
            }
            else
            {
                ::PostMessage(g_hwnd, WM_HIDEINPUTWND, 0, 0);
            }
        }

    }

}

Windows server2016实现

经过测试发现,上述实现,在windows10、Windows11直接去解析ImmersiveFocusTracking回调就可以了,但是在Windows server2016以及在Windows server2019上非触屏的情况下无法正常实现该功能(在触屏点击的情况下可以正常实现功能),经过打印日志,发现只有在非输入状态切换到输入状态下ImmersiveFocusTracking回调才会被调用,而编辑框失去焦点的情况下,该回调是无法被调用的,那猜测应该是调用了其他回调函数,转到s_WnfCallback函数:

这里看回调函数列表应该就是保存在static变量s_rgHandlerMappings里面,调到变量定义:

看到这个变量的偏移是F2640,对于静态变量内存地址其实就是模块地址+变量偏移:

在插件里面打印s_rgHandlerMappings变量地址、模块地址:

这个模块地址是0x00007FFDBA3A0000,s_rgHandlerMappings变量地址是:0x00007FFDBA492640。用任务管理器dump TabTip.exe:

 然后在windbg dumps_rgHandlerMappings变量地址0x00007FFDBA492640:

第一行00007ffdba3b2740地址偏移12740(s_OnImmersiveFocusTracking的函数偏移),前面的0f840539a3bc1835刚好是这个回调的ID。

第三行00007ffdba3f7b20地址偏移57B20(s_OnTouchEvent的函数偏移),前面的0f840539`a3bc2035应该是这个回调的ID。

第五行同样是00007ffd`ba3f7b20,前面的回调ID是0f840539`a3bc2835。

第七行00007ffd`ba3b6610地址偏移1 6610(s_OnLanguage的函数偏移),前面的回调是0f840539`a3bc3035。

继续往后面进行dump:

第一行00007ffd`ba3b6590地址偏移16590(s_OnForegroundWindow的函数偏移),对应前面的ID是0f840539`a3bc3835。

第三行00007ffd`ba3f7ab0地址偏移5 7AB0(s_OnModernKeyboardFocusTracking的函数偏移),对应的ID是0f840539`a3bc5035。

第五行00007ffd`ba3b8db0地址偏移1 8DB0(s_OnCaretTracking函数的地址偏移),对应ID是0f840539`a3bc4035

第七行00007ffd`ba3f78d0地址偏移5 78D0(s_OnAutocomplete函数的地址偏移),对应ID是0f840539`a3bc4835

继续往后面进行dump:

第一行00007ffd`ba3f7950地址偏移5 7950(s_OnCandidateWindowState函数地址偏移),对应的ID是0f840539`a3bc7835。

第三行00007ffd`ba3f7a80地址偏移5 7A80(s_OnLauncherVisible)对应ID是0f950324`a3bc1035

第五行00007ffd`ba3f7a30地址偏移5 7A30(s_OnKillLogicalFocus)对应ID是0f950324`a3bc3035

第七行00007ffd`ba3c0f80地址偏移2 0F80(s_OnImmersiveMonitorChanged)对应ID是0f950324`a3bc1835

继续dump内存:

第一行00007ffd`ba3f7970地址偏移5 7970(s_OnCompositionState),对应ID是0f840539`a3bc9035

第三行00007ffd`ba3b2740地址偏移1 2740(s_OnImmersiveFocusTracking)对应ID是0f840539`a3bc9835

 

转到ProcessWindowMessage:

0x42Bu其实就是1067,这个函数里面找到相应的处理函数:

但是经过分析发现,当弹起软键盘的时候,用鼠标点击其他地方,软键盘也会收起,同时触发0f840539`a3bc9035这唯一的一条回调ID,不过经过试验发现,屏蔽该回调函数,软键盘依旧会收起,说明该回调ID不是起到收起软键盘的回调,收起软键盘的处理函数在其他地方。

 重新回到_OnImmersiveFocusTracking函数,这个函数之前是主要处理输入事件回调的:

再到看到处理函数CIPTipWnd::ProcessWindowMessage:

经过hook另一个函数:CImmersiveVisibilityTracker::_SetVisibility:

发现当参数 a2和*a3为1的时候会弹出软键盘,为0的时候自动收起软键盘:

 堆栈上看,应该是MSG为0x436,用windbg附加到进程,输入命令:bp USER32!PostMessageW "k;.echo 'breaks';g":

通过在server2016上打印堆栈(弹出软键盘):

processWindowMessage+0xbcf对应代码为:

隐藏软键盘堆栈:

proceeswindowmessage+0xffa对应代码:

 0x413哪里来的呢,打印相应的堆栈信息:

这里转到LowLevelMouseHookProc的代码:

 这个LowLevelMouseHookProc其实就是一个全局的鼠标钩子:

经过分析,编写相关代码:

(WM_LBUTTONUP=0x202(514)    WM_RBUTTONUP=0x205(517))

测试发现还是对的上的,鼠标左键或右键弹起的时候会触发发送0x413消息:

总结:windows server2016上,软键盘的收起是通过安装全局鼠标钩子,在鼠标左键或右键弹起的时候让软键盘收起来实现的。

Windows server 2016  WNF事件探索

windows 10、11等系统是通过创建1ImmersiveFocusTrackingActiveEvent来实现的,只要有这个event,注册的WNF回调就会被系统调用到。但是windows server 2016并不起作用,需要重新进行探索。

经过分析,缩小代码范围,发现在调用tabskins模块的 CIPTipWnd::InitializeUI之前,注册的WNF回调是不会收到回调的,而在调用完该函数后,注册的WNF回调就能正常收到回调。

而这个函数调用了CNotificationHandler::Initialize,CNotificationHandler::Initialize继而调用SubscribeWnfStateChangeNotification来注册WNF回调。

进一步缩小范围发现在调用 CIPTipWnd::InitializeUI过程中,在调用 CNotificationHandler::Initialize之前,注册的WNF回调就能正常收到回调。说明范围在这里。

通过进一步缩小范围,发现在CIPTipWnd::CreateFrame函数执行里面;

经过不断缩小范围,发现是在 CIPTipWnd::StartCaretTracking(CIPTipWnd *this)里面,进一步又缩小到tiptsf.dll模块的StartCaretTracking函数。

转到导入表发现导入了tiptsf模块的四个函数:

AdviseHook的调用:

然后通过hook这两个函数,并打印传参信息:

 然后就可以编写相应的代码了:

 经过测试,发现ok了,这样就算是找到了server2016设置wnf事件的方法了。

但是经过发现,重启系统后,仅仅通过上述方法还是不行,需要启动TabTip.exe,并且发现是在tipskins的StartCaretTracking到tiptsf.dll的StartCaretTracking之间,执行了某个函数才生效的:

 经过分析,发现是在因为CIPTipWnd::_IsCaretTrackingEnabled(this) 返回了TRUE导致的。

经过不断分析和试错,发现其实上面的V3就是名字叫1DefaultTIPSharedMemory的FileMap文件映射的buffer,而*(_DWORD*)(V3+44)就把这个内存设置为1,于是代码如下:

g_hShareMemoryFileMap = OpenFileMappingW(FILE_MAP_WRITE  | FILE_MAP_READ , 0, g_szBufTipSharedMemoryName);
if (!g_hShareMemoryFileMap)
{
    g_hShareMemoryFileMap = CreateFileMappingW((HANDLE)-1, NULL, PAGE_READWRITE, 0, 0x68u, g_szBufTipSharedMemoryName);
}
if(g_hShareMemoryFileMap)
{
    LPVOID lpViewFile = MapViewOfFile(g_hShareMemoryFileMap, FILE_MAP_WRITE | FILE_MAP_READ, 0, 0, 0);
    if (lpViewFile)
    {
        if (*(DWORD*)((__int64)lpViewFile + 44) == FALSE)
        {
            if (IDYES == MessageBox(NULL, L"1DefaultTIPSharedMemory flag is 0,是否设置为1?", L"tips", MB_YESNO))
            {
                 *(DWORD*)((__int64)lpViewFile + 44) = 0x01;
            }
        }
    }
}

实现效果预览

       动画效果视频链接动画1-CSDN直播

(图上,win11上的实现效果)

(图上,windows server 2016实现效果)

总结

1.系统输入状态检测是通过ntdll的未公布的API:RtlSubscribeWnfStateChangeNotification注册一个ID为WNF_TKBN_IMMERSIVE_FOCUS_TRACKING({ 0xA3BC1835, 0x0F840539 })的回调函数,然后通过解析tipskins.dll模块的_OnImmersiveFocusTracking回调函数里面的buffer参数来实现对系统输入状态切换的监听的。

2.win10及以后的版本包括win11,win server 2019版本只需要解析_OnImmersiveFocusTracking函数以及解析里面的回调buffer即可达到系统输入的切入以及切出状态的监听。但是在win server 2016上,系统输入状态的切出动作不会通过回调通知到应用程序,需要另外通过全局的鼠标钩子来进行另外的处理。

3.win10及以后的版本包括win11,win server 2019版本开启WNF事件只需要创建名为“1ImmersiveFocusTrackingActiveEvent”的Event并设置这个Event事件来开启的(如果不开启这个事件,我们注册的回调函数就不会收到任何通知);而win server 2016则更加复杂,第一因为它安装了很多键盘和鼠标的全局钩子,一旦调试进程就会导致系统特别卡顿,所以TabTip进程是不方便断点调试的,第二TabTip是通过创建名为“1DefaultTIPSharedMemory”的命名管道并把偏移未44的地址设置为1,同时必须调用tiptsf.dll模块的AdviseHook以及StartCaretTracking这两个接口。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值