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 <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上测试都能正常运行。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 最全的iOS面试题及答案 iOS面试小贴士 ———————————————回答好下面的足够了-----------...
    zweic阅读 2,686评论 0 73
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,278评论 0 6
  • 1.写一个NSString类的实现 +(id)initWithCString:(c*****t char *)nu...
    韩七夏阅读 3,739评论 2 37
  • 多线程、特别是NSOperation 和 GCD 的内部原理。运行时机制的原理和运用场景。SDWebImage的原...
    LZM轮回阅读 2,001评论 0 12
  • 有时候,我觉得,推己及人,以及,推人及己,是一件很善良的事。我抵临生活本质的目的,不是穷形尽相,觉得这个世界没法看...
    Sunny飞镜阅读 115评论 0 0