Chernobyl
Learning
基于Ring3的行为分析工具开发日志——进程通信

起因

趁还有印象赶紧动笔

程序行为分析器采用DLL注入的方式实现,类似于

hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPID));
pRemoteBuf=VirtualAllocEx(hProcess,NULL,dwBufSize,MEM_COMMIT,PAGE_READWRITE);
WriteProcessMemory(hProcess,pRemoteBuf,szDLLName,dwBufSize,NULL);
hMod=GetModuleHandle("kernel32.dll");
pThreadProc=(LPTHREAD_START_ROUTINE)GetProceAddress(hMod,"LoadLibraryW");
hThread=CreateRemoteThread(hProcess,NULL,0,pThreadProc,pRemoteBuf,0,NULL);

每个Dll内部都有保存当前进程运行信息的数据作为记录和缓存

string processname;
map<Handle, ProcessName>ProcMap;
...

在快速原型的构建中,采用了日志模块作为输出和Debug,在输出目标为文件的时候,获取信息十分简单,不需要考虑进程通信的问题

BOOL MyProcessApi(...)
{
    LogToFile<<....
}

运行时的架构大概类似于这种

DllInjector.exe----->注入程序,注入后可退出
Target.exe(被注入程序)
|Thread1...->a.log
|Thread2...->a.log
|...
|Target2.exe(由Target1创建)
||Thread1...->a.log
||....

完成快速原型,打算用QT5套上一层皮的时候,获取信息就不像写日志那么简单了,需要跨进程获取信息。

DllInjector.exe<-----<
Target.exe(被注入程序) |
|Thread1...--------->|
|Thread2...->a.log   ^
|...       ----------|
|Target2.exe(由Target1创建)
||Thread1...---------|
||....      ---------|

最开始的想法是在Injector里面声明一个方法作为 DLL的回调函数,在初始化DLL的时候将全局变量的指针作为参数传入Naive

//Injector.cpp
map<Handle, ProcessName>ProcMap;
void get_data(const map<Handle, ProcessName>& in)
{
    in[...] = ...;
}

int wmain(int argc, wchar_t** argv)
{
    ...;
    Injectdll(Target, &ProcMap, (PVOID)get_data);
    while(true)
    {
        cout<<ProcMap[...];
        Sleep(2000);
    }
}

//Inject.dll
BOOL MyProcessApi(ProcMap, Pget_data)
{
    void(*)(const map<Handle, ProcessName>&)(Pget_data)(ProcMap);
    ...
}
.....

看到目标程序崩的时候愣了,WinDbg连上去看了下,发现在Dll的进程空间内,ProcMapget_data都是NULL。保护模式下的进程隔离,直接传地址凉凉,引用也不行。得跳出之前单进程编程的桎梏,找找多进程下数据传输和同步的思路。

进程通信方案

Pipe

介绍

管道是进程之间通信的抽象通道,管道通信具有单向性,单工串行。在Windows下,对管道的操作被抽象为对文件的操作。

实现的大致思路如下:

//Injector
int wmain(int argc, wchar_t** argv)
{
    // 创建命名管道
    HANDLE hPipe = NULL;
    hPipe = CreateNamedPipe( EXAMP_PIPE,
        PIPE_ACCESS_DUPLEX, 
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,               
        PIPE_UNLIMITED_INSTANCES, 
        BUF_SIZE,         
        BUF_SIZE,
        0,
        NULL); 
    ....;
    memset(szBuffer, 0, BUF_SIZE);
    while(true)
        ReadFile(hPipe,szBuffer,BUF_SIZE,&dwReturn,NULL);//循环读取数据
}

//Dll
BOOL WINAPI DllMain(
  _In_ HINSTANCE hinstDLL, // 指向自身的句柄
  _In_ DWORD fdwReason, // 调用原因
  _In_ LPVOID lpvReserved // 隐式加载和显式加载
)
{
    switch(fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            hPipe = CreateFile(EXAMP_PIPE,
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                NULL,
                OPEN_EXISTING,
                0,
                NULL);//打开命名管道
            ....
    }
}
MyProcessApi()
{
    ...;
    WriteFile(WriteFile(hPipe,szBuffer,strlen(szBuffer),&dwReturn,NULL));//发送数据
}

问题

首先是管道的阻塞问题。通过ReadFile读取就意味着在管道的一端需要将数据序列化,另一端反序列化。当Injector来不及处理管道中的数据,导致管道缓冲区满的时候会导致Dll中的WriteFile阻塞。阻塞后会引发某些系统API调用过程中KeWaitForSingleObject超时,导致程序无响应并退出。在Dbg中调用堆栈类似于

f7b838a0 8006554b 8204752c 00000000 00000000 nt!KeWaitForSingleObject+0x1a3
f7b838c0 8046bd50 82014f80 f7b83bf4 f7b83c10 nt!WriteFile+0x2b
f7b838e8 8046bb4d e1edb3c8 00000000 8044fbd3 KernelBase!WriteFile+0x180
f7b838f4 8044fbd3 e1edb3c8 8044f89c e1edb3c8 Kernel32!WriteFile+0xb

为了解决阻塞尝试过许多性能优化,比如inline序列化函数、-O3、位操作符判断等,结果依然不行。Dll中监控了几十个函数,包括WriteFile,也就是说在写管道的时候需要进行额外的API地址复原操作,感觉优化到了瓶颈。下图是Wanacry运行两分钟的数据量:

其次是数据的处理问题。使用非阻塞的管道虽然可以避免阻塞导致的超时,但是会导致序列化与反序列化十分麻烦,需要校验传输的数据长度,解析缓冲区中结束字符串的个数等等——传入的数据本身就是变长的,校验个P。

不过在数据量少的时候,管道的确是相对容易实现且透明的方案。

造轮子失败

Dll共享内存

原理

实现原理比较简单,就是先为DLL创建一个数据段,然后再对程序的链接器进行设置,使其在程序编译完毕开始链接的时候,根据设置的链接选项,把指定的数据段链接为共享数据段,调用Dll声明导出函数获取数据:

\Dll
#pragma data_seg("MySeg")
    char g_szText[256] = {0};
#pragma data_seg()
#pragma comment(linker, "/section:MySeg,RWS")

extern "C" char* __declspec(dllexport) __stdcall Getdata();
...

问题

不支持STL,在共享段声明STL对象会导致主程序读取错误,因为Stl中默认的内存分配并不限定于某一段内存,特别是Map,平衡二叉树结构,插入元素时内存分配是不连续的。重写下allocator?掉头发。

malloc重写new的堆分配,请

共享内存

原理

共享内存的方式原理就是将一份物理内存映射到不同进程各自的虚拟地址空间上,这样每个进程都可以读取同一份数据,从而实现进程通信。

//Injector
int wmain(int argc, wchar_t** argv)
{
    const DWORD dwMemoryFileSize = 4 * 1024;  //指定内存映射文件大小
    const LPCTSTR sMemoryFileName = L"ShareMemory";//指定内存映射文件名称
    m_hFileMapping = CreateFileMapping(//创建映射
        INVALID_HANDLE_VALUE,           
        NULL,                           
        PAGE_READWRITE,                 
        0,                              
        dwMemoryFileSize*sizeof(TCHAR), 
        sMemoryFileName
        ); 
    Inject();
    ...;
    LPVOID lpBase = MapViewOfFile(
        m_hFileMapping,            // 共享内存的句柄
        FILE_MAP_ALL_ACCESS, // 可读写许可
        0,
        0,
        BUF_SIZE
        );
    strcpy(Buffer,(char*)lpBase);//读取内存
    .....
}

//Dll
BOOL WINAPI DllMain(
  _In_ HINSTANCE hinstDLL, 
  _In_ DWORD fdwReason, 
  _In_ LPVOID lpvReserved 
)
{
    switch(fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            HANDLE hMapFile = OpenFileMapping(
            FILE_MAP_ALL_ACCESS,
            NULL,
            L"ShareMemory"
            );//打开共享内存
    }
}
MyProcessApi()
{
    ...
    strcpy((char*)lpBase,Data);//写入数据
}

当时写完感觉这波啊,这波稳了,共享内存效率没得说,只要把STL放进里面…..额。

问题

首先,与Dll共享内存一样,用malloc重写new的堆分配,将STL内存分配在固定区域的同时保持性能,请。

退一步来说,不使用STL实现,另一个不能忽视的是数据同步问题,同一块内存区域,多个DLL共同读写,是利用信号同步还是异步读写?异步如何处理数据完整性,同步如何处理超时?对不同的Dll进行区域分块,静态还是动态?大小如何确定?比如:

\Inject->1.exe
strcpy((char*)lpBase,Data);

\\Inject->2.exe
strcpy((char*)lpBase+0x200,Data);//为每个dll分配固定空间->超出部分会导致覆写,长度检查导致数据截断

\\Inject->2.exe
strcpy((char*)lpBase/DllNum, Data);//每个Dll平均分配动态空间->空间利用率、重新分配空间的数据迁移

解决这些比Dll共享内存还复杂。说到底还是用malloc重写new的堆分配,请

WM_COPYDATA

WM_COPYDATAWindows下面向窗口对象的消息传递方式,其底层实现原理是文件映射,即对内存共享的封装。与共享内存相比,WM_COPYDATA的实现较为简单,可以通过结构体来自定义数据,但是代价是阻塞,所发送的数据不能包含数据接收方无法访问的指针或对象引用(需要复制拷贝),消息发送后,在函数返回前要保证lpData所引用数据不能被其它线程修改。实现方法如下:

//Head
typedef struct tagCOPYDATASTRUCT {
  ULONG_PTR dwData;//自定义数据
  DWORD     cbData;//数据结构体大小
  PVOID     lpData;//缓冲区
} COPYDATASTRUCT, *PCOPYDATASTRUCT;

//Injector
BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_INITDIALOG://初始化,设置窗口标题
          SetWindowText(hDlg, L"RecvMessage");
          s_hEditShowRecv = GetDlgItem(hDlg, IDC_EDIT_RECVMESSAGE);
          return TRUE;
    case WM_COPYDATA://接受WM_COPYDATA的回调函数
          COPYDATASTRUCT *pCopyData = (COPYDATASTRUCT*)lParam;
          ......
          return TRUE; 
    }
    return FALSE;     
}

int WinMain(HINSTANCE hInstance,//控制台程序无法注册消息回调函数
        HINSTANCE hPrevInstance,//需要使用Winmain入口创建自定窗口对象
        LPSTR     lpCmdLine,//或者在控制台中新建隐藏窗口
        int       nCmdShow)
{
   DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);//创建对话框输出数据
   ....
   return 0;
}

//Dll
MyProcessApi()
{
    HWND Injector = FindWindow(NULL,L"RecvMessage");//获取窗口的句柄
    .....
    SendMessage(Injector, WM_COPYDATA, NULL, (LPARAM)&Data);
    ....
}

最终的实现效果类似下图

问题

与管道一样,存在数据阻塞的问题,DLL在调用SendMessage发送后会调用接受窗口的处理流程,直到处理完后SendMessage才会返回值。

其次是权限的问题,在Window Vista之后,发送WM_COPYDATA的程序需要管理员权限——也就是如果注入的样本不需要管理员权限运行就无法往外发送消息。同时,低权限向高权限的程序发送消息也会被拒绝——在程序分析器的注入模型中,消息要么不能流动,要么只能由DLL->Injector单向流动。

EOF
首页      兴趣      项目实践      基于Ring3的行为分析工具开发日志——进程通信

Chernobyl

文章作者

发表评论

textsms
account_circle
email

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

基于Ring3的行为分析工具开发日志——进程通信
起因 趁还有印象赶紧动笔 程序行为分析器采用DLL注入的方式实现,类似于 hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPID)); pRemoteBuf=VirtualAllocEx(hProcess,NULL,dwBufSiz…
扫描二维码继续阅读
2020-02-16