第 9 课:主控内嵌日志进程与通信机制
# 第 9 课:主控内嵌日志进程与通信机制
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课只讲主控侧日志进程的内嵌、启动和 IPC 通信机制,不讲被控 shellcode,不讲插件加载链路,也不提供任何未授权使用步骤。
# 1. 本课要解决什么问题
第 8 课讲了主控端架构。第 9 课只聚焦一个更具体的问题:主控程序为什么要额外启动一个 log.exe,这个日志进程从哪里来,主控又如何把日志消息写给它。
本课按三个问题展开:
- 日志进程从哪里来:主控内嵌了日志进程数据,启动时把它写成
log.exe。 - 日志进程如何启动:主控调用
CreateProcess启动独立子进程。 - 主控如何与日志进程通信:主控用
osIPC::Client连接日志进程创建的共享内存和信号量,再通过logger->write写日志。
这节课容易和第 18 课混淆。第 18 课讲的是被控 shellcode 如何生成和执行;本课讲的是主控自己的日志子进程。两者不是同一条链路。

# 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
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
对新手来说,这段代码可以拆成两层:
str_log_Path = 当前目录 + "\\log.exe",确定日志进程文件名。- 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);
}
}
}
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
这段代码有两个安全工程意义:
- 如果日志进程启动失败,主控会退出,说明日志进程对主控运行很重要。
- 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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 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; };
};
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));
}
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 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) // 信息
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);
}
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");
// 后续列表插入逻辑省略。
}
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. 合法练习题
- 只阅读源码,画出
MainFrm.cpp -> log.exe -> osIPC::Client -> logger->write的数据流。 - 说明为什么主控使用日志子进程时,父子进程关系是重要证据。
- 根据
ipc.cpp,解释共享内存名和信号量名如何从日志进程 PID 派生。 - 给合规远程运维工具设计一条日志字段规范,至少包含操作者、目标主机、功能名、时间和结果。
# 11. 本课小结
第 9 课讲的是主控自己的日志链路:主控把内嵌的日志进程数据写成 log.exe,通过 CreateProcess 启动独立子进程,再用 osIPC::Client 连接日志进程创建的共享内存和信号量,最后通过 logger->write 写入日志消息。
这节课和被控 shellcode 无关,也不需要和插件加载链路混在一起。对企业安全工程来说,它的价值在于识别“主进程写出并启动辅助 EXE、再通过 IPC 交换日志”的完整证据链。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言