48 / 52 安全检测、响应与重构

第 48 课:C/C++ 安全编码核查

# 第 48 课:C/C++ 安全编码核查

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

前面的专栏主要讲“模块能做什么”。本课换一个角度:即使不讨论功能意图,C/C++ 代码本身也可能因为长度校验、内存释放、句柄生命周期、线程参数、权限掩码和错误处理不严谨而产生稳定性和安全问题。新手读 C/C++ 安全代码时,不要只看程序能不能跑,还要看失败路径是否可靠、资源是否释放、输入是否可信。 第 48 课:C/C++ 安全编码核查(配图)

# 1. 本课阅读方法

本课围绕 6 类问题展开:

类别 新手要问的问题
长度校验 memcpy 前有没有确认输入长度足够
内存生命周期 new/deletemalloc/free 是否匹配
句柄释放 Open* 后每条返回路径是否关闭
线程同步 线程参数和共享变量生命周期是否稳定
权限最小化 是否滥用 KEY_ALL_ACCESSPROCESS_ALL_ACCESS
错误处理 失败时是否清理资源并返回明确状态

# 2. 源码目录和文件

位置 作用 本课关注
主插件\启动管理\启动管理\StartupManager.cpp 启动项命令处理 网络包长度与结构体复制
主插件\解密数据\解密数据\DecryptManger.cpp 敏感数据格式化 mallocdelete 不匹配
主插件\注入管理\注入管理\InjectManager.cpp 远程进程操作 句柄释放、权限过宽
主插件\HPSocket\Buffer.cpp 缓冲区实现 长度、内存重分配、指针移动
主插件\文件管理\文件管理\FileManager.cpp 文件和注册表操作 宽权限、路径和错误处理

依赖说明: 本课不是讲某一个第三方库,而是从 C/C++ 资源管理角度横向看多个模块。涉及 HPSocket 缓冲区、Windows 句柄、注册表 API、内存分配函数和注入模块中的远程进程 API。所有判断都放进代码注释,例如长度来源、释放方式、句柄关闭路径和权限掩码,不再单独列独立栏目。

# 3. 长度校验:结构体复制前先判断 nSize

3. 长度校验:结构体复制前先判断 nSize(配图)

网络包是不可信输入。任何从 lpBuffer 里直接 memcpy 出结构体的代码,都应该先判断 nSize 是否足够。下面这段代码把 lpBuffer 复制到 StartupRequest,但节选中没有看到复制前的长度判断。

// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp,约121-128 case COMMAND_STARTUP_REGISTRY_REQUEST:
{
    StartupRequest startupRequest;
    memcpy(&startupRequest, lpBuffer, sizeof(startupRequest));

    bool success = SetRegistryStart(startupRequest.enable);

    OnHandleStartupResponse(
        COMMAND_STARTUP_REGISTRY_RESPONSE,
        startupRequest.enable,
        success);

    // 防御分析:从网络包复制结构体前,应确认 nSize >= sizeof(StartupRequest)    // 否则异常包可能造成越界读或错误解析}
1
2
3
4
5
6
7
8
9
10
11
12
13

同类问题在多个分支中出现。

// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp,约131-161 case COMMAND_STARTUP_FOLDER_REQUEST:
{
    StartupRequest startupRequest;
    memcpy(&startupRequest, lpBuffer, sizeof(startupRequest));
    bool success = CopyOrRemoveFromStartupDir(startupRequest.enable);
}
break;

case COMMAND_STARTUP_SERVICE_REQUEST:
{
    StartupRequest startupRequest;
    memcpy(&startupRequest, lpBuffer, sizeof(startupRequest));
    bool success = AddOrDeleteService(startupRequest.enable);
}
break;

// 防御分析:相同解析模式重复出现时,应抽成统一的安全解析函数,
// 先校验长度,再解析字段,再执行业务逻辑?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

建议写法思路

步骤 要求
先判断 token 只确认命令类型,不读取后续字段
再判断长度 nSize >= sizeof(结构体)
再复制结构体 使用明确的结构体版本
再检查字段 布尔、枚举、长度字段都要限制范围
最后执行 业务函数不直接接收原始网络缓冲区

# 4. 内存生命周期:申请和释放必须匹配

4. 内存生命周期:申请和释放必须匹配(配图)

下面这段代码使用 malloc 申请内存,但释放函数使用 delete。这属于释放方式不匹配,可能引发未定义行为。

// 源码位置:主插件\解密数据\解密数据\DecryptManger.cpp,约132-172 char* CDecryptManger::GetCookiesChar(vector<BrowserData>* pPass, int* memLen)
{
    int size = 1;
    for (vector<BrowserData>::iterator iter = (*pPass).begin(); iter != (*pPass).end(); iter++)
    {
        size += int((*iter).bro_name.size()) + 17;
        size += int((*iter).bro_url.size()) + 17;
        size += int((*iter).user_name.size()) + 17;
        size += int((*iter).pass_word.size()) + 17;
    }

    char* pCharPass = (char*)malloc(sizeof(char) * size);
    memset(pCharPass, 0, size);

    // 防御分析:这里的关键不是 pCharPass 这个变量名,而是生命周期配对    // malloc 申请的内存应free 释放;new delete,new[] delete[]    // VirtualAlloc VirtualFree。更稳妥的工程写法是使用 std::string    // std::vector<char> 或智能指针,让释放动作随对象生命周期自动发生    return pCharPass;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 源码位置:主插件\解密数据\解密数据\DecryptManger.cpp,约230-238 void CDecryptManger::DeleteMem(char** pChromeC)
{
    if (*pChromeC != nullptr)
    {
        delete *pChromeC;
    }

    // 防御分析:这个函数和 GetCookiesChar 形成一条完整证据链    // 上游malloc 申请,下游却delete 释放,释放方式不匹配    // 这种问题不需要单独列成表格,直接在申请点和释放点标注即可    // 修复方向是:要么改成 free(*pChromeC),要么把申请和释放都改为 RAII}
1
2
3
4
5
6
7
8

# 5. 句柄生命周期和过宽权。

注入管理模块中使用 OpenProcess(PROCESS_ALL_ACCESS)。从代码质量看,过宽权限会扩大风险;从资源管理看,成功打开的句柄需要保证所有路径释放。

// 源码位置:主插件\注入管理\注入管理\InjectManager.cpp,约948-963 HANDLE hProc = OpenProcess(
    PROCESS_ALL_ACCESS,
    FALSE,
    m_sinjectmode.dwProcessID);

if (hProc == NULL)
{
    SendError(_T("打开进程失败"));
    return;
}

LPBYTE lpAddress = new BYTE[dwSize];
if (lpAddress == NULL)
{
    SendError(_T("VirtualAlloc Error"));
    CloseHandle(hFile);
    return;
}

// 防御分析:这里打开了目标进程句柄// 后续每条失败路径都应关闭 hProc,权限也应改成最小需要集合?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 源码位置:主插件\注入管理\注入管理\InjectManager.cpp,约970-979 LPVOID RemotlpAddress = VirtualAllocEx(
    hProc,
    NULL,
    dwSize,
    MEM_COMMIT,
    PAGE_EXECUTE_READWRITE);

WriteProcessMemory(hProc, RemotlpAddress, lpAddress, dwSize, NULL);
HANDLE ThreadShellCode = CreateRemoteThread(
    hProc,
    NULL,
    0,
    (LPTHREAD_START_ROUTINE)RemotlpAddress,
    NULL,
    0,
    NULL);

if (ThreadShellCode)
    CloseHandle(ThreadShellCode);

// 防御分析:这段只用于说明资源和权限问题// 远程进程操作属于高风险行为,企业代码中应默认禁止?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

5. 句柄生命周期和过宽权。(配图)

# 6. 缓冲区管理:重分配和指针移动

缓冲区类使用 VirtualAllocCopyMemoryMoveMemory 管理数据。新手读这类代码时,要重点看长度来源、指针边界和异常路径。

// 源码位置:主插件\HPSocket\Buffer.cpp,约36-50 BOOL CBuffer::Write(PBYTE pData, UINT nSize, BOOL bXORrecoder, byte* password)
{
    ReAllocateBuffer(nSize + GetBufferLen());
    CopyMemory(m_pPtr, pData, nSize);

    if (bXORrecoder)
    {
        for (int i = 0, j = 0; i < (int)nSize; i++)
        {
            ((char*)m_pPtr)[i] ^= (password[j++]) % 456 + 54;
            if (i % (10) == 0)
                j = 0;
        }
    }

    // 防御分析:nSize 来自外部调用者,调用者必须保证长度可信    // 更稳妥的实现应检ReAllocateBuffer 返回值}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 源码位置:主插件\HPSocket\Buffer.cpp,约74-94 if (nSize > GetMemSize())
    return 0;

if (nSize > GetBufferLen())
    nSize = GetBufferLen();

if (nSize)
{
    CopyMemory(pData, m_pBase, nSize);
    MoveMemory(m_pBase, m_pBase + nSize, GetMemSize() - nSize);
    m_pPtr -= nSize;
}

DeAllocateBuffer(GetBufferLen());

// 防御分析:移动内存时应使用有效数据长度,而不是总分配大小// 缓冲区代码建议配套单元测试覆盖边界长度?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7. 权限最小化:不要默ALL_ACCESS

文件管理中打开注册表使用 KEY_ALL_ACCESS。如果只是查询文件关联,不需要这么宽的权限。

// 源码位置:主插件\文件管理\文件管理\FileManager.cpp,约480-500 if (RegOpenKeyEx(
        HKEY_CLASSES_ROOT,
        lpExt,
        0L,
        KEY_ALL_ACCESS,
        &hKey) != ERROR_SUCCESS)
    return false;

RegQueryValue(hKey, NULL, strTemp, &nSize);
RegCloseKey(hKey);

// 防御分析:读取注册表时应优先使用 KEY_READ// KEY_ALL_ACCESS 会扩大权限面,也会提高被安全产品拦截的概率?

1
2
3
4
5
6
7
8
9
10
11
12
13

# 8. 更具体的修复写法

本课前面指出了几个问题,这里给出更贴近工程的修复方向。下面代码是防御性示例,用来说明如何把“先校验、再解析、最后执行”的思路固化到代码里。

// 示例位置:可用于 StartupManager.cpp 中解StartupRequest // 防御分析:网络包进入业务函数前,先做长度和命令校验// 这样业务函数拿到的是可信结构体,而不是原lpBufferbool TryReadStartupRequest(
    const BYTE* lpBuffer,
    UINT nSize,
    BYTE expectedCommand,
    StartupRequest* outRequest)
{
    if (lpBuffer == NULL || outRequest == NULL)
        return false;

    if (nSize < 1 + sizeof(StartupRequest))
        return false;

    if (lpBuffer[0] != expectedCommand)
        return false;

    memcpy(outRequest, lpBuffer + 1, sizeof(StartupRequest));

    // 防御分析:布尔字段也要归一化,避免异常值进入业务分支    outRequest->enable = outRequest->enable ? TRUE : FALSE;
    return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

句柄释放也可以用小型封装降低失败路径遗漏。重点不是引入复杂框架,而是确保 CloseHandle 不依赖每个 return 分支手工记忆。

// 示例位置:可用于涉及 HANDLE 的管理类或局部工具
// 防御分析:ScopedHandle 用析构函数释放句柄,减少失败路径泄漏
// 这类封装适合文件、进程、线程、服务控制管理器等 Windows 句柄
class ScopedHandle
{
public:
    explicit ScopedHandle(HANDLE value = NULL) : handle(value) {}
    ~ScopedHandle()
    {
        if (handle != NULL && handle != INVALID_HANDLE_VALUE)
            CloseHandle(handle);
    }

    HANDLE get() const { return handle; }

private:
    HANDLE handle;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

最小权限可以按“当前函数真正需要什么”来拆。例如只读取注册表值时使用 KEY_READ,只写指定值时使用 KEY_SET_VALUE,不要默认使用 KEY_ALL_ACCESS

原始写法 更具体的替代 说明
KEY_ALL_ACCESS 读取文件关联 KEY_READ 只读场景不需要创建、删除、写入权限
PROCESS_ALL_ACCESS 查询进程 PROCESS_QUERY_LIMITED_INFORMATION 查询信息不应申请全权限
CreateFile(..., GENERIC_READ | GENERIC_WRITE) 按实际动作拆分 只读、只写、追加写分别限制
直接 memcpy 结构 TryRead* 解析函数 统一处理长度、版本、字段范

# 9. 企业检测点

层面 检测点 说明
静态扫 memcpyCopyMemorystrcat_swsprintf 检查长度来
内存 newmallocVirtualAlloc 检查释放方式和失败路径
句柄 OpenProcessCreateFileRegOpenKeyEx 检查权限和关闭路径
线程 _beginthreadexCreateThread 检查参数生命周期
权限 ALL_ACCESS 类掩码 优先改成最小权限

# 10. 合法练习:

  1. 从任意一个插件中找 3 处 memcpy,说明它们的长度来源是否可信?

  2. malloc/delete 不匹配的问题改写为 std::stringstd::vector<char> 的设计说明?

  3. 列出 KEY_READKEY_ALL_ACCESS 的差异,并说明为什么读取注册表不应使用后者?

  4. 设计一个“网络包安全解析函数”的伪代码:输入缓冲区、长度、结构体版本,输出解析结果。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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