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 <Windows.h>
#include <processthreadsapi.h>
#include <iostream>
#include <map>
#include <tuple>
using namespace std;
static map<int, int>* val;
static void update(void);
static int dump(char* str, int bufsize);
static void update(void) {
map<int, int>& pv = *val;
pv[0] = 0;
pv[1] = 0;
pv[2] = 0;
while (true) {
pv[0] += 1;
pv[1] += 3;
pv[2] += 2;
char buf[200];
dump(buf, 200);
std::cout << "Hello World: " << buf << endl;
Sleep(1000);
}
}
static int dump(char* str, int bufsize) {
map<int, int>& pv = **(&val);
char b[200];
int n = sprintf_s(b, "{\"num\": %d, \"time\": %d, \"avg\": %d}", pv[0], pv[1], pv[2]);
if (bufsize < n)
return n;
strcpy_s(str, bufsize, b);
return 0;
}
static int dump1(void* arg) {
char* str;
int bufsize;
ULONG64* pu = (ULONG64*)arg;
str = (char*)arg;
bufsize = (int)pu[0];
int ret = dump(str, bufsize);
return ret;
}
static bool incrPriv() {
LUID luidTmp;
HANDLE hToken;
TOKEN_PRIVILEGES tkp;
// 提权
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
cout << "AdjustProcessTokenPrivilege OpenProcessToken Failed " << GetLastError() << endl;
return false;
}
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luidTmp))
{
cout << "AdjustProcessTokenPrivilege LookupPrivilegeValue Failed " << GetLastError() << endl;
CloseHandle(hToken);
return false;
}
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = luidTmp;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL))
{
cout << "AdjustProcessTokenPrivilege AdjustTokenPrivileges Failed " << GetLastError() << endl;
CloseHandle(hToken);
return false;
}
CloseHandle(hToken);
return true;
}
static void monitor(int pid) {
if (!incrPriv())
return;
//
HANDLE hProcess = NULL;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
if (hProcess == NULL) {
cout << "OpenProcess Failed " << GetLastError() << endl;
return;
}
SIZE_T ret = 0;
void* ptr = NULL;
auto ok = ReadProcessMemory(hProcess, &val, &ptr, sizeof(&val), &ret);
if (!ok) {
cout << "ReadProcessMemory Failed " << GetLastError() << endl;
CloseHandle(hProcess);
return;
}
cout << "process " << pid << " val ptr " << ptr << endl;
int bufsize = 4096;
LPVOID lpRemoteBuf = VirtualAllocEx(hProcess, NULL, bufsize, MEM_COMMIT, PAGE_READWRITE);
if (lpRemoteBuf == NULL) {
cout << "VirtualAllocEx Failed " << GetLastError() << endl;
CloseHandle(hProcess);
return;
}
ok = WriteProcessMemory(hProcess, lpRemoteBuf, &bufsize, sizeof(bufsize), &ret);
if (!ok || ret != sizeof(bufsize))
{
cout << "WriteProcessMemory Failed " << GetLastError() << endl;
VirtualFreeEx(hProcess, lpRemoteBuf, bufsize, MEM_COMMIT);
CloseHandle(hProcess);
return;
}
DWORD dwNewThreadId;
HANDLE hNewRemoteThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)dump1, lpRemoteBuf, 0, &dwNewThreadId);
if (hNewRemoteThread == NULL)
{
cout << "CreateRemoteThread Failed " << GetLastError() << endl;
VirtualFreeEx(hProcess, lpRemoteBuf, bufsize, MEM_COMMIT);
CloseHandle(hProcess);
return;
}
cout << "CreateRemoteThread Succeed " << endl;
WaitForSingleObject(hNewRemoteThread, INFINITE);
CloseHandle(hNewRemoteThread);
char* str = new char[bufsize];
ok = ReadProcessMemory(hProcess, lpRemoteBuf, (void*)str, bufsize, &ret);
if(ok)
cout << "get dump: " << str << endl;
else
cout << "ReadProcessMemory Failed " << GetLastError() << endl;
VirtualFreeEx(hProcess, lpRemoteBuf, bufsize, MEM_COMMIT);
CloseHandle(hProcess);
}
/*
* usage:
* task-main: cross-process
* monitor: cross-process {pid}
*/
int main(int argc, char*argv[])
{
if (argc == 1) {
val = new map<int, int>;
cout << "i am " << GetCurrentProcessId() << endl;
cout << "val addr " << &val << ", val ptr " << val << endl;
update();
}
else if (argc == 2) {
int pid = 0;
if (sscanf_s(argv[1], "%d", &pid) == 1 && pid > 0) {
monitor(pid);
}
else {
cout << "error: invalid pid" << endl;
return EINVAL;
}
}
else {
cout << "error: invalid arguments" << endl;
}
return 0;
}
3、测试结果
测试时,首先无参数启动一个进程模拟客户端在运行,一段时间后将进程号作参数执行命令,抓取一次监控数据并输出。
在win10上测试结果如下。
如上图所示,左侧是模拟客户端运行,一直在更新数据,右侧是监控程序,每执行一次便会抓取一次数据,从结果对比可见,抓取是成功的。
为验证兼容性,程序还在win7,win2008, win2012上测试都能正常运行。