1、背景
想要开发一个客户端软件的性能监控工具,原来的想法是客户端创建一个对外监听的服务,外部程序通过HTTP或者进程间通信来获取数据,这样的不好是要修改客户端代码、增加了很多额外的工作量。后来了解到windows有提供远程注入的调用,可以直接在另一个进程内创建线程并获取数据,于是便有了新的想法,便是不修改客户端,创建一个新的监视工具,在客户端创建线程和分配内存,根据客户端全局变量的虚拟地址找到需要监控的指标,执行代码输出后返回到监视工具,从而实现无侵入的监控客户端。
为测试这个想法,做了一个简单的原型程序验证。
先介绍几个用到的关键点。
1.1 全局变量的虚拟地址不变
在windows下,可执行程序里的全局变量的虚拟地址是固定的,因此同一个程序启动不同进程,全局变量的虚拟地址是一样的。这个特点的用处就是,只要全局变量布局一样,便可以使用本进程的全局变量虚拟地址在远程进程内使用,从而进一步获取更多的数据。
1.2 OpenProcess()
接口打开一个本地进程,返回一个句柄。如果需要获取全部权限,还需要调用SeDebugPrivilege()提权。
函数原型。
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
dwDesiredAccess: 希望在远程进程获得的权限,简单的就用PROCESS_ALL_ACCESS表明需要全部权限,具体参见微软官方接口声明。
bInheritHandle: 是否允许子进程继承这个句柄
dwProcessId: 远程进程号
1.3 CreateRemoteThread()
本接口将在远程进程创建一个线程。
函数原型。
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
重点关注的参数是hProcess,lpStartAddress,lpParameter,其余都可以用默认值0或者NULL。hProcess是远程进程句柄,lpStartAddress是线程入口函数,必须是远程进程内存在的函数,lpParameter是线程函数参数。
更多解释参见微软官方接口声明。
1.4 VirtualAllocEx()
该接口功能强大,可以在远程进程内分配内存并返回虚拟地址。
原型。
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
更多解释参见微软官方接口声明。
1.5 VirtualFreeEx()
释放VirtualAllocEx()分配的内存,原型如下。
BOOL VirtualFreeEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD dwFreeType
);
参数意思明确,不做过多解读,更多解释参见微软官方接口声明。
1.6 ReadProcessMemory()
本接口从远程进程复制内存到本进程缓冲区。原型如下。成功返回非0,失败返回0。
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesRead
);
lpBaseAddress是远程进程的虚拟地址,lpBuffer是本进程缓冲区。更多解释参见微软官方接口声明。
1.7 WriteProcessMemory()
本接口将本进程缓冲区复制到远程进程。原型如下。成功返回非0,失败返回0。
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesWritten
);
更多解释参见微软官方接口声明。
1.8 提权
打开另外一个进程需要一定的权限,为了尽量保证打开成功,需要调用一些接口将本进程的权限提高,具体参见代码。
2、主要实现
原型程序同时包含模拟客户端和监控程序的功能,逻辑比较简单,有一个全局变量val作监控指标,有两个函数分别对应模拟客户端和监控的功能,一个是不停更新val,相当于是客户端软件在运行,另一个是监控功能,需要一个进程号作参数,抓取这个进程的监控指标并显示出来。
在软件编写过程中,需要十分注意的便是,本地函数在另外一个进程执行时,所有变量地址都是远程进程内的地址,在本进程是无效的,反过来也是一样,所以传入远程线程的参数必须是远程进程的指针,指针无效可能会导致远程进程崩溃。
在这个实现中,为了实现数据的传入传出,首先在远程进程内申请了一个内存,将参数写入到这个内存的首部,远程线程函数从这个内存获取远程进程内有效参数后才能正确运行。
主要代码如下。
// cross-process.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
/*
* 使用OpenProcess(),ReadProcessMemory(),WriteProcessMemory(),CreateRemoteThread(),VirtualAllocEx()等接口,
* 实现非侵入的获取另外一个进程的数据,实现非侵入式实时监控
*/
#include
#include
#include
#include
3、测试结果
测试时,首先无参数启动一个进程模拟客户端在运行,一段时间后将进程号作参数执行命令,抓取一次监控数据并输出。
在win10上测试结果如下。
如上图所示,左侧是模拟客户端运行,一直在更新数据,右侧是监控程序,每执行一次便会抓取一次数据,从结果对比可见,抓取是成功的。
为验证兼容性,程序还在win7,win2008, win2012上测试都能正常运行。