I’m watching you! How to spy Windows users via MS UIA
相信大家都会同意,跟踪用户一直是一个有趣的挑战。这就像探索他们的秘密、发现密码,以及挖掘那些仅依赖于人为因素的攻击场景。 我们很高兴能分享我们关于使用用户界面自动化框架进行用户监视的最新研究。在本文中,我们将带您深入了解 Windows 组件对象模型,创建自定义事件处理程序,并学习 Windows 图形表示树等内容。
我们还开发了一个概念验证工具,名为 Spyndicapped。
什么是 MS UIA?
MS UIA(Microsoft 用户界面自动化)是一个专门设计用于自动化操作 Windows 图形用户界面的框架。通过它,你可以读取屏幕上的任何文本值,打开菜单,关闭窗口。
总的来说,你可以执行用户在显示器前所做的一切操作,就像黑客亲自坐在桌面前一样。
该框架最初是为残障人士设计的,这也是我们的工具名字 Spyndicapped 的由来。
在 MS UIA 之前,Windows 主要使用 MSAA(Microsoft Active Accessibility),但现在已经完全被 MS UIA 取代。值得注意的是,从 Windows XP 开始的几乎所有现代系统都支持 MS UIA,所以我认为没有必要深入研究 MSAA。
你可以在这里了解更多关于 MSAA 和 UIA 的比较信息。
基本结构
元素、属性等
让我们来看看基本结构。
该框架主要由几个核心组件组成:元素、属性、事件和模式。
元素是你在屏幕上看到的所有内容,如文本、按钮、菜单等。属性定义了每个元素的特性。例如,元素的 Name 属性包含元素的名称,Value 属性是其值,Flags 是各种附加标志。相信你已经理解了这个概念。
这有点像 Windows 内置的图形引擎,但是专门用于 UIA。你可以处理任何与图形用户界面相关的事件,比如数据输入、属性更改或窗口打开。
最后,模式提供了与元素进行实时交互的机会,这真的很酷。例如,Scroll 模式允许你通过程序代码滚动滚动条,而 Invoke 模式则模拟点击按钮时发生的函数调用。
Windows 中元素的图形表示
(也称为自动化树或控件树)
Windows 中的所有图形元素都有严格的层级结构。层级的根节点是当前桌面。桌面的子节点是显示的窗口。窗口的子节点是窗口内的控件。而在控件内部可能还有其他按钮、菜单和用户交互的其他机制。
这种树状结构对于搜索项目、遍历树或处理结构变化非常方便。在 Spyndicapped 中有很多与树操作相关的方法,你可以在专门的 MyTreeWalker 类中找到它们。
深入了解 COM
UIA 的工作只能基于相应的 COM 类。你也可以用 C# 开发程序,但我选择了 C++,因为我喜欢它,也喜欢在内存管理中犯很多错误 :)。
从全局来看,我们需要使用多个接口:
-
IUIAutomation — 允许我们开始使用 COM。可以通过调用 CoCreateInstance() 函数获得; -
IUIAutomationElement — 指向特定的图形元素。可以通过处理 UIA 事件或通过树遍历获得(稍后会详细介绍); -
IUIAutomationCondition — 允许你创建搜索所需元素时使用的条件。例如,你可以创建一个条件来查找名为 Misha123 或值为 Cicada8 的元素; -
IUIAutomationTreeWalker — 用于遍历图形表示树。
值得注意的是,用户在屏幕上只能看到 IUIAutomatomatioElement。其他都是辅助接口。
我们的代码几乎都将基于处理事件、获取必要的 IUIAutomationElement 和输出它们的值。
这些接口的方法将帮助我们完成这些任务,但让我们先熟悉一下事件处理程序。
事件处理
在我之前描述的所有实体之上,还有两个全局角色:UIA 客户端和 UIA 提供程序。UIA 客户端订阅和处理从 UIA 提供程序接收到的事件。如果你编写提供程序,你可以生成自己的 UIA 事件。但我们将编写一个客户端,因为对于概念验证来说,处理 Windows 内置 UIA 提供程序提供的事件就足够了。
可以处理的事件类型相当多。而且必须通过合适的提供程序来进行处理。
IUIAutomationEventHandler 用于处理基本的 UIA 事件。这些事件包括打开窗口、调用函数等。总的来说,就是所有以 UIA_ 开头并以 EventId 结尾的常量。
在注册处理程序时,你必须明确指定要跟踪哪些事件。如果不指定任何事件,你将无法接收通知。处理程序注册是通过 IUIAutomationEventHandler 接口的 AddAutomationEventHandler() 函数完成的,你需要向该函数传递处理程序函数的地址和要处理的事件。
以下是注册示例。
std::vector<EVENTID> eventIds = {
UIA_AutomationPropertyChangedEventId,
UIA_Text_TextSelectionChangedEventId,
UIA_Text_TextChangedEventId,
UIA_Invoke_InvokedEventId,
UIA_Window_WindowOpenedEventId,
};
for (size_t i = 0; i < eventIds.size(); i++)
{
HRESULT hr = pAutomation->AddAutomationEventHandler(eventIds[i], pAutomationElement, TreeScope_Subtree, NULL, (IUIAutomationEventHandler*)pMyAutomationEventHandler);
if (FAILED(hr)) {
Log(L"Failed to add event handler for event ID: " + std::to_wstring(eventIds[i]), WARNING);
PrintErrorFromHRESULT(hr);
return E_ABORT;
}
}
在此之后,处理程序函数将被调用。以下是处理程序示例。
HRESULT STDMETHODCALLTYPE MyAutomationEventHandler::HandleAutomationEvent(IUIAutomationElement* pAutomationElement, EVENTID eventID)
{
...
}
-
pAutomationElement — 生成事件的元素。例如,如果输入框开始接收数据,它就会是这个输入框; -
eventID — 触发的事件。
Spyndicapped 工具还注册了一个属性处理程序(IUIAutomationPropertyChangedEventHandler)。它允许你跟踪特定程序(如 chrome.exe 或 keepass.exe)注册的属性变化。
可以通过 AddPropertyChangedEventHandler() 或 AddPropertyChangedHandlerNativeArray() 添加这样的处理程序。
以下是注册示例。
std::vector<int> propertyIds = {
UIA_NamePropertyId,
UIA_ValueValuePropertyId,
UIA_SelectionItemIsSelectedPropertyId,
};
SAFEARRAY* propertyArray = SafeArrayCreateVector(VT_I4, 0, propertyIds.size());
if (!propertyArray)
{
Log(L"Can't create property arrray", WARNING);
return E_ABORT;
}
for (size_t i = 0; i < propertyIds.size(); i++)
{
long index = static_cast<long>(i);
SafeArrayPutElement(propertyArray, &index, &propertyIds[i]);
}
hr = pAutomation->AddPropertyChangedEventHandler(pAutomationElement, TreeScope_Subtree, NULL, pMyPropertyChangedEventHandler, propertyArray);
if (FAILED(hr)) {
Log(L"Failed to add property changed event handler" , WARNING);
PrintErrorFromHRESULT(hr);
return E_ABORT;
}
以下是处理程序函数。
HRESULT STDMETHODCALLTYPE MyPropertyChangedEventHandler::HandlePropertyChangedEvent(IUIAutomationElement* pAutomationElement, PROPERTYID propId, VARIANT vVar)
{
...
}
-
pAutomationElement - 生成事件的元素。例如,如果是输入框开始接收数据,它就会是这个输入框; -
propId - 修改的属性 ID; -
vVar - VARIANT 格式的新值。
这些绝不是全部的处理程序。你可以创建用于通知、焦点变化、文本位置变化等更多事件的处理程序。以下是完整列表:
-
IUIAutomationChangesEventHandler - 任何变化; -
IUIAutomationActiveTextPositionChangedEventHandler - 文本位置变化; -
IUIAutomationTextEditTextChangedEventHandler - 通过自动完成和其他视障辅助机制的文本修改; -
IUIAutomationFocusChangedEventHandler - 焦点变化; -
IUIAutomationStructureChangedEventHandler - 控件树的变化 (例如添加元素); -
IUIAutomationNotificationEventHandler - 通知处理。
使用 UIA 进行开发辅助
如何理解按下按键时发生了什么事件?或者应该访问哪个图形元素来解决我们的问题?几个工具 (包括前面提到的_inspect.exe_和_accevent.exe_) 可以帮助我们。
WinSpy
链接
这是著名的 Spy++ 调试图形元素工具的扩展版本。要查看 UIA 树,点击_View Automation Tree_按钮:
Inspect.exe
链接
我更喜欢 Inspect.exe 而不是 WinSpy。这个工具随 Windows SDK 一起提供。它显示了 Windows 图形树的所有必要数据。
你可以点击这些按钮让项目高亮显示,这对定位正确的按钮非常方便!
你可以在这里了解更多关于 inspect.exe 的用法。
accevent.exe
链接
使用这个应用程序,你可以跟踪 Windows 中的所有 UIA 事件。实际上,它是一个通用的 UIA 客户端!使用非常简单。首先,点击模式并选择"UIA Events"。
然后点击"Settings"按钮,选择你想要接收的事件和事件属性。
然后转到"Events" -> "Start Listening"
你就会开始接收事件。
然后点击"Stop Listening"按钮开始分析事件。
如何制作你自己的隐蔽记录器
在我的研究过程中,我识别了输入或处理文本数据时发生的主要事件,之后我创建了两个处理程序:
-
MyAutomationEventHandler.cpp; -
MyPropertyChangedEventHandler.cpp。
第一个用于处理一般的 UIA 事件,第二个用于处理属性变化事件。
然后我学会了从事件中识别它来自哪个进程。我通过以下函数实现:
-
Finder::GetPIDByUIAutomationElement(); -
Finder::GetModuleNameFromPid();
之后我在 lambda 函数上实现了一个简单的路由机制。根据进程名称,会调用名为_
if (g_IgnoreHandlers)
{
HandleOther(pAutomationElement, wsProcName, wsEventString, wsDate, eventID);
}
else {
std::unordered_map<std::wstring, std::function<void()>> handlers = {
{ L"firefox.exe", [this, pAutomationElement, wsProcName, wsEventString, wsDate, eventID]() { HandleFirefox(pAutomationElement, wsProcName, wsEventString, wsDate, eventID); } },
{ L"explorer.exe", [this, pAutomationElement, wsProcName, wsEventString, wsDate, eventID]() { HandleExplorer(pAutomationElement, wsProcName, wsEventString, wsDate, eventID); } }
};
auto it = handlers.find(Helpers::ConvertToLower(wsProcName));
if (it != handlers.end()) {
it->second();
}
else {
HandleOther(pAutomationElement, wsProcName, wsEventString, wsDate, eventID);
}
}
例如,如果事件在 MyAutomationEventHandler.cpp 处理程序中接收到,将会调用 MyAutomationEventHandlerApps.cpp 中的函数。
然后在每个函数处理程序内部,我为每个进程创建了独特的逻辑。
例如,如果在 firefox.exe 进程中发生了变化,会检查当前标签页的域名。如果是 web.whatsapp.com 或 app.slack.com,那么它会处理该网站以找到消息内容和接收者的名称。
std::unordered_map<std::wstring, std::function<void()>> handlers = {
{ L"web.whatsapp.com", [this, pAutomationElement, wsProcName, wsDate]() { HandleWhatsAppFF(pAutomationElement, wsProcName, wsDate); } },
{ L"app.slack.com", [this, pAutomationElement, wsProcName, wsDate]() { HandleSlackFF(pAutomationElement, wsProcName, wsDate); } }
};
auto it = handlers.find(Helpers::ConvertToLower(wsDomain));
if (it != handlers.end()) {
it->second();
}
else {
HandleOther(pAutomationElement, wsProcName, wsEventString, wsDate, eventID);
}
因此,你可以拦截几乎任何潜在的有趣文本内容。
除此之外,我还为 KeePass.exe 进程添加了一个单独的处理程序。如果 Spyndicapped 检测到打开的 KeePass 窗口,它会通过快速模拟"复制密码"按钮自动检索当前标签页中的所有密码。所有这些都仅通过 MS UIA 工具完成,这使得该工具可能对 EDR 来说相当隐蔽。
我的工具几乎使用了所有成功开发 MS UIA 所需的概念。你可以轻松查看源代码并修改 Spyndicapped,以实现对 Windows 上几乎任何应用程序的自动化跟踪!
原文始发于微信公众号(securitainment):我在看着你!如何通过 MS UIA 监视 Windows 用户获取敏感信息
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论