关键字
注入dll,遍历ListView
技术调研背景
QA测试程序时,往往需要关注进程的性能指标,比如:CPU,GPU,内存,显存。最终根据各个采样数据,生成基于时间轴的状态表(类似任务管理器的性能图表)
难点:
- 通过代码中调用API,某些进程详情信息很难获取(比如GPU占用率和显存使用量),而且这需要修改测试程序的代码:在项目中定时获取相关指标。
- 人工定时获取相关指标不现实(之前QA是间隔几十秒手动记录)当然后来利用系统工具(性能计数器)可以实现部分信息的定时记录。
线索及相关测试结果
1. 任务栏管理器的“详细信息”页面,是使用ListView的UI控件实现的,其class name是“SysListView32”,获取到该列表的HWND句柄后,通过遍历ListView可以读取所有行列信息。
ListView的相关API:ListView_GetColumn macro (commctrl.h) - Win32 apps | Microsoft Learn
2. 注意:因为任务栏管理器是基于管理员权限运行的,访问其相关对象需要自己的进程也以管理员权限启动。
3. 获取到ListView句柄后(可以通过SPY抓取 或遍历适配任务栏管理器的所有子窗口),利用API VirtualAllocEx + WriteProcessMemory,尝试跨进程读取ListView的对象,但是失败了。参考:
通过句柄获取或遍历SysListView32和SysTreeView32控件的数据信息_浮世的博客-CSDN博客_systreeview32
4. 既然跨进程访问ListView不可行,尝试注入dll到任务栏管理器进程,在注入的dll中遍历ListView的行列信息,测试成功!(注意:注入dll的执行进程 必须也以管理员权限启动 否则无法注入dll到任务管理器)
缺点:
该方法是基于UI上的窗口句柄HWND的遍历获取,用户必须显式打开任务管理器 并将页面切换到“详细信息”。
运行结果截图:
在注入dll中的测试代码如下所示。注意:因为系统封装的ListView相关的宏接口,都是用SendMessage访问list属性的,所以这些宏接口都可以在非UI线程调用。
#include <Windows.h>
#include <Commctrl.h>
#define TEXTSIZE 256
void TestFunc()
{
// 该句柄即是任务管理器的“详细信息”页面的列表控件句柄,可以用spy抓取,也可以遍历所有子控件并通过claaname判断:“SysListView32”
HWND hListViewOfProcessDetail = (HWND)0X00202666;
//获得窗口类名, 任务管理器的“详细信息”的list控件,其classname是“SysListView32”
TCHAR lpClassName[TEXTSIZE];
GetClassName(hListViewOfProcessDetail, (LPWSTR)lpClassName,
TEXTSIZE - 1);
// 遍历各个列的索引号和列名
// 注意:此处测试结果发现,实际的列索引并不是UI上显示的索引顺序,而是“选择列”页面上已经选中的项的索引号。
for (size_t i = 0; i < 10; i++) {
TCHAR buf[TEXTSIZE] = {0};
LV_COLUMN temp;
temp.mask = LVCF_TEXT;
temp.pszText = buf;
temp.cchTextMax = TEXTSIZE;
ListView_GetColumn(hListViewOfProcessDetail, i, &temp);
if (buf[0] == 0)
break;
wchar_t txt[200];
swprintf_s(txt, 200, L"index:%d columnName:%s", (int)i, buf);
//MessageBox(0, txt, txt, 0);
}
// 获取列表的总行数
int nCount = ListView_GetItemCount(hListViewOfProcessDetail);
for (int i = 0; i < nCount; ++i) { // 遍历每一行
TCHAR processName[TEXTSIZE] = {0}; // exe name
LV_ITEM lvi;
// 指定列的索引号,索引0始终是进程名称列。
// 注意:索引号并不是UI上所看到的从左到右的索引。
// 查找索引号的方法:打开“详细信息”的“选择列”窗口,
// 该窗口从上到下,所选中的项,其列索引以1开始依次递增
// (索引0始终是进程“名称”这一列的index)
lvi.iSubItem = 0;
lvi.pszText = processName;
lvi.cchTextMax = TEXTSIZE;
SendMessage(hListViewOfProcessDetail, LVM_GETITEMTEXT, i,
(LPARAM)&lvi);
MessageBox(0, processName, L"wangshaohui test", 0);
}
};
列索引示意图(设置给LV_ITEM::iSubItem的索引值)
结束语:
据悉,也可以用python获取任务管理器的进程详情,尚未调查确认。