9 / 52 Windows、主控与网络

第 9 课:主控内嵌日志进程与通信机制

# 第 9 课:主控内嵌日志进程与通信机制

免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。

本课只讲主控侧日志进程的内嵌、启动和 IPC 通信机制,不讲被控 shellcode,不讲插件加载链路,也不提供任何未授权使用步骤。

# 1. 本课要解决什么问题

第 8 课讲了主控端架构。第 9 课只聚焦一个更具体的问题:主控程序为什么要额外启动一个 log.exe,这个日志进程从哪里来,主控又如何把日志消息写给它。

本课按三个问题展开:

  1. 日志进程从哪里来:主控内嵌了日志进程数据,启动时把它写成 log.exe
  2. 日志进程如何启动:主控调用 CreateProcess 启动独立子进程。
  3. 主控如何与日志进程通信:主控用 osIPC::Client 连接日志进程创建的共享内存和信号量,再通过 logger->write 写日志。

这节课容易和第 18 课混淆。第 18 课讲的是被控 shellcode 如何生成和执行;本课讲的是主控自己的日志子进程。两者不是同一条链路。

1. 本课要解决什么问题(配图)

# 2. 相关源码目录

文件 作用
主控\Quick\MainFrm.cpp 主控启动阶段写出 log.exe、创建日志进程、初始化 IPC 客户端
主控\Quick\ipc.h IPC 的 Server / Client 类定义,说明共享内存、信号量和缓冲区结构
主控\Quick\ipc.cpp osIPC::Client 连接共享内存、写入日志块的具体实现
主控\Quick\stdafx.h logger 全局指针和 log_严重log_错误log_警告log_信息
主控\Quick\LogView.cpp 主控界面日志视图,只负责显示和导出,不是日志产生源头

阅读顺序建议是:先看 MainFrm.cpp 的启动代码,再看 ipc.h/ipc.cpp,最后看日志宏和日志视图。这样不会把“日志展示”和“日志进程通信”混在一起。

# 3. 日志进程从哪里来

主控启动时会先定位自身目录,再准备一些运行需要的虚拟文件。日志进程路径被拼成当前目录下的 log.exe

// 源码位置:主控\Quick\MainFrm.cpp:394-416
TCHAR szSelfPath[MAX_PATH];
GetModuleFileName(NULL, szSelfPath, ARRAYSIZE(szSelfPath));
CString str_path = szSelfPath;
str_path = str_path.Mid(0, str_path.ReverseFind('\\'));

CString str_screenshot, str_avcode, str_avutil, str_log_Path;
str_screenshot = str_path + _T("\\screenshot");
str_avcode = str_path + _T("\\avcodec-57.dll");
str_avutil = str_path + _T("\\avutil-55.dll");
str_log_Path = str_path + _T("\\log.exe");

DWORD dwTemp = 0;
::CreateDirectory(str_screenshot, NULL);

// 防御分析:这里使用 BoxedApp SDK 的虚拟文件接口写出依赖 DLL。
// 这说明主控不是简单依赖磁盘旁边已有文件,而是在启动阶段准备运行环境。
HANDLE h_avcodefile = (*MyBoxedAppSDK_CreateVirtualFileW)(
    str_avcode,
    GENERIC_WRITE,
    FILE_SHARE_READ,
    NULL,
    CREATE_NEW,
    0,
    NULL);
WriteFile(h_avcodefile, avcodecMyFileBuf, avcodecMyFileSize, &dwTemp, NULL);

#ifdef _WIN64
    // 防御分析:源码注释说明 x64 的 log.exe 内嵌数据尚未生成,先从磁盘启动。
#else
    // 防御分析:x86 分支把内嵌的 logMyFileBuf 写成 log.exe。
    // 用户说的“日志进程以 shellcode/内嵌数据形式进入主控”,对应的重点就是这里:
    // logMyFileBuf/logMyFileSize 是被编进主控的数据,启动时再落成日志进程文件。
    HANDLE h_logfile = (*MyBoxedAppSDK_CreateVirtualFileW)(
        str_log_Path,
        GENERIC_WRITE,
        FILE_SHARE_READ,
        NULL,
        CREATE_NEW,
        0,
        NULL);
    WriteFile(h_logfile, logMyFileBuf, logMyFileSize, &dwTemp, NULL);
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

对新手来说,这段代码可以拆成两层:

  1. str_log_Path = 当前目录 + "\\log.exe",确定日志进程文件名。
  2. x86 分支把 logMyFileBuf 写入这个路径,形成一个可启动的日志程序。

这里的重点不是被控 shellcode,而是主控把日志程序作为内嵌数据随主控一起携带。企业审计时,应把“主进程启动后写出 EXE 子进程”作为行为链的一部分记录下来。

# 4. 主控如何启动日志进程

写出日志进程后,主控通过 CreateProcess 启动它。日志进程是独立进程,不是主控进程里的线程。

// 源码位置:主控\Quick\MainFrm.cpp:420-447
PROCESS_INFORMATION* pi = new PROCESS_INFORMATION;
STARTUPINFO si = { sizeof(si) };

// 防御分析:CreateProcess 的命令行指向 str_log_Path,也就是前面准备好的 log.exe。
// 这一步会形成主控进程 -> log.exe 的父子进程关系。
BOOL ret = CreateProcess(
    NULL,
    str_log_Path.GetBuffer(),
    NULL,
    NULL,
    FALSE,
    0,
    NULL,
    NULL,
    &si,
    pi);

DWORD iPid = GetCurrentProcessId();

// 防御分析:主控最多重试 50 次连接日志进程 IPC。
// 连接地址使用日志进程 PID 字符串,说明日志进程会按自身 PID 创建 IPC 对象名。
for (int i = 0; i < 50; i++)
{
    char temp_char[10] = {};
    _itoa_s(pi->dwProcessId, temp_char, 10);
    logger = new osIPC::Client(temp_char);

    if (logger->IsOk())
    {
        // 防御分析:连接成功后创建监视线程,后续用于检查日志进程是否还存在。
        HANDLE hThread;
        hThread = CreateThread(NULL, 0, ThreadProc, pi, 0, NULL);
        CloseHandle(hThread);
        break;
    }
    else
    {
        SAFE_DELETE(logger);
        Sleep(100);
        if (i > 45)
        {
            AfxMessageBox(_T("日志进程打开失败,结束进程"));
            exit(0);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

这段代码有两个安全工程意义:

  1. 如果日志进程启动失败,主控会退出,说明日志进程对主控运行很重要。
  2. IPC 连接地址来自日志进程 PID,主控和日志进程之间存在明确绑定关系。

# 5. 主控如何把日志目录告诉日志进程

IPC 建好后,主控会把自己的 PID 和日志目录发给日志进程。这样日志进程既知道“谁是主控”,也知道“日志写到哪里”。

// 源码位置:主控\Quick\MainFrm.cpp:453-466
char szFilePath[MAX_PATH] = { 0 }, szDrive[MAX_PATH] = { 0 }, szDir[MAX_PATH] = { 0 };
char szFileName[MAX_PATH] = { 0 }, szExt[MAX_PATH] = { 0 };
GetModuleFileNameA(NULL, szFilePath, ARRAYSIZE(szFilePath));
_splitpath_s(szFilePath, szDrive, szDir, szFileName, szExt);

// 防御分析:日志目录固定为主控所在目录下的 Log\。
CStringA str(szDrive);
str = str + szDir + "Log\\";

// 防御分析:第一段数据 = 主控 PID + 日志目录字符串。
// 日志进程收到后可据此绑定主控并落盘日志。
int loginfosize = sizeof(DWORD) + str.GetLength() + 1;
void* buff = new char[sizeof(logInfo)];
ZeroMemory(buff, sizeof(logInfo));
memcpy(buff, &iPid, sizeof(DWORD));
memcpy((char*)buff + sizeof(DWORD), str.GetBuffer(), str.GetLength() + 1);
logger->write(buff, loginfosize);
SAFE_DELETE_AR(buff);

// 防御分析:随后发送两条可读日志,分别作为标题和提示信息。
CString titlestr = _T("  程序BUG查询使用   ");
logger->write(titlestr, 4);
titlestr.Format(_T("日志进程PID %d   请勿关闭"), pi->dwProcessId);
logger->write(titlestr, 3);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

5. 主控如何把日志目录告诉日志进程(配图)

# 6. IPC 通信结构:共享内存 + 信号量

osIPC::Client 不是普通 socket。它通过共享内存和信号量把日志块交给日志进程。

// 源码位置:主控\Quick\ipc.h:146-174
class Client
{
private:
    Stat    m_stat;

    char*   m_sAddr;       // 防御分析:IPC 地址。主控传入的是日志进程 PID 字符串。
    HANDLE  m_hMapFile;    // 防御分析:共享内存文件映射句柄。
    HANDLE  m_hSignal;     // 防御分析:通知日志进程“有数据可读”的信号量。
    HANDLE  m_hAvail;      // 防御分析:通知主控“有空闲块可写”的信号量。
    MemBuff* m_pBuf;       // 防御分析:映射后的共享内存缓冲区。
    int     datasize;

public:
    DWORD write(void* pBuff, DWORD amount, DWORD dwTimeout = INFINITE);
    void  write(char* str, int mode);
    void  write(CString str, int mode);
    BOOL  IsOk(void) { if (m_pBuf) return true; else return false; };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

客户端构造函数会根据传入地址拼出三个名字:<PID>_evt_avail<PID>_evt_filled<PID>_mem。这说明日志进程一侧应该提前创建了对应对象,主控只是打开并写入。

// 源码位置:主控\Quick\ipc.cpp:417-505
osIPC::Client::Client(const char* connectAddr)
{
    m_sAddr = (char*)malloc(IPC_MAX_ADDR);
    ZeroMemory(m_sAddr, IPC_MAX_ADDR);
    strcpy_s(m_sAddr, IPC_MAX_ADDR, connectAddr);

    // 防御分析:用同一个地址派生出两个信号量和一个共享内存名。
    // 如果 connectAddr 是日志进程 PID,那么 IPC 对象名也带有这个 PID。
    char* m_sEvtAvail = (char*)malloc(IPC_MAX_ADDR);
    sprintf_s(m_sEvtAvail, IPC_MAX_ADDR, "%s_evt_avail", m_sAddr);

    char* m_sEvtFilled = (char*)malloc(IPC_MAX_ADDR);
    sprintf_s(m_sEvtFilled, IPC_MAX_ADDR, "%s_evt_filled", m_sAddr);

    char* m_sMemName = (char*)malloc(IPC_MAX_ADDR);
    sprintf_s(m_sMemName, IPC_MAX_ADDR, "%s_mem", m_sAddr);

    // 防御分析:主控打开日志进程创建的共享内存对象。
    m_hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,
        FALSE,
        charToWchar(m_sMemName));

    // 防御分析:MapViewOfFile 后,m_pBuf 就是主控和日志进程共享的缓冲区。
    m_pBuf = (MemBuff*)MapViewOfFile(
        m_hMapFile,
        FILE_MAP_ALL_ACCESS,
        0,
        0,
        sizeof(MemBuff));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

日志写入本质上是“取一个空闲块、复制数据、通知对端”。

// 源码位置:主控\Quick\ipc.cpp:613-635
DWORD osIPC::Client::write(void* pBuff, DWORD amount, DWORD dwTimeout)
{
    // 防御分析:先等待一个可写块,防止多个日志消息互相覆盖。
    Block* pBlock = getBlock(dwTimeout);
    if (!pBlock)
        return 0xffffffff;

    // 防御分析:日志数据复制到共享内存块中,最大不超过 IPC_BLOCK_SIZE。
    DWORD dwAmount = min(amount, IPC_BLOCK_SIZE);
    memcpy(pBlock->Data, pBuff, dwAmount);
    pBlock->Amount = dwAmount;

    // 防御分析:postBlock 会释放 m_hSignal,让日志进程知道可以读取。
    postBlock(pBlock);
    return dwAmount;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

6. IPC 通信结构:共享内存 + 信号量(配图)

# 7. 日志宏如何落到 IPC

业务代码通常不会直接调用 osIPC::Client::write,而是使用 stdafx.h 中的日志宏。

// 源码位置:主控\Quick\stdafx.h:103-108
extern osIPC::Client* logger;

// 防御分析:这些宏把日志级别编码成数字,并统一写给日志进程。
// 好处是业务代码调用简单;风险是如果 logger 不可信或不可用,日志链路会失效。
#define log_严重(x)  if(logger) logger->write(x,1) // 严重
#define log_错误(x)  if(logger) logger->write(x,2) // 错误
#define log_警告(x)  if(logger) logger->write(x,3) // 警告
#define log_信息(x)  if(logger) logger->write(x,4) // 信息
1
2
3
4
5
6
7
8
9

write(CString, int) 会把字符串和级别封装成 logInfo 后写入共享内存。

// 源码位置:主控\Quick\ipc.cpp:646-653
void osIPC::Client::write(CString str, int mode)
{
    logInfo mlogInfo;
    mlogInfo.mode = mode; // 防御分析:日志级别,来自 log_严重 / log_错误 / log_警告 / log_信息。

    // 防御分析:CString 按 TCHAR 宽度复制,写入共享内存前已经成为结构化日志块。
    memcpy(mlogInfo.text, str.GetBuffer(), str.GetLength() * 2 + 2);
    write((void*)&mlogInfo, datasize);
}
1
2
3
4
5
6
7
8
9
10

# 8. 日志视图不是日志进程

LogView.cpp 负责主控界面里的日志展示和导出。它不是日志进程启动和 IPC 的核心,但它能帮助读者理解日志最后如何显示出来。

// 源码位置:主控\Quick\LogView.cpp:296-306
void CLogView::InsertLogItem(
    LPCTSTR Text0,
    LPCTSTR Text1,
    LPCTSTR Text2,
    LPCTSTR Text3,
    LPCTSTR Text4,
    LPCTSTR Text5,
    LPCTSTR Text6,
    LPCTSTR Text7)
{
    // 防御分析:这里是界面展示层,负责把日志字段插入列表控件。
    // 它不是日志产生源头,也不是 IPC 发送端。
    char m_Text[512] = { 0 };
    CTime time = CTime::GetCurrentTime();
    CString strTime = time.Format(" %Y-%m-%d %H:%M:%S");
    // 后续列表插入逻辑省略。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 9. 企业检测点

层面 检测点 说明
文件侧 主控启动后出现 log.exe 关注写出路径、父进程、文件哈希和写出时间
进程侧 主控创建 log.exe 子进程 记录父子进程关系,确认是否符合产品设计
IPC 侧 以日志进程 PID 派生的共享内存和信号量 关注 <PID>_mem<PID>_evt_* 这类命名模式
日志侧 logger->write 频繁写入 关注日志级别、消息内容、是否可追溯操作者
稳定性 日志进程失败会导致主控退出 合规工具需要明确故障降级和提示机制

# 10. 合法练习题

  1. 只阅读源码,画出 MainFrm.cpp -> log.exe -> osIPC::Client -> logger->write 的数据流。
  2. 说明为什么主控使用日志子进程时,父子进程关系是重要证据。
  3. 根据 ipc.cpp,解释共享内存名和信号量名如何从日志进程 PID 派生。
  4. 给合规远程运维工具设计一条日志字段规范,至少包含操作者、目标主机、功能名、时间和结果。

# 11. 本课小结

第 9 课讲的是主控自己的日志链路:主控把内嵌的日志进程数据写成 log.exe,通过 CreateProcess 启动独立子进程,再用 osIPC::Client 连接日志进程创建的共享内存和信号量,最后通过 logger->write 写入日志消息。

这节课和被控 shellcode 无关,也不需要和插件加载链路混在一起。对企业安全工程来说,它的价值在于识别“主进程写出并启动辅助 EXE、再通过 IPC 交换日志”的完整证据链。

安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>