windows无侵入获取其它进程的数据

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 
#include 

using namespace std;

static map* val;

static void update(void);
static int dump(char* str, int bufsize);

static void update(void) {
    map& 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& 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;
        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上测试都能正常运行。

你可能感兴趣的:(windows无侵入获取其它进程的数据)