21、基础 | 程序员必看的525钟C++重点

《360 安全规则集合》简称《安规集》,是一套详细的 C/C++ 安全编程指南,由 360 集团质量工程部编著,将编程时需要注意的问题总结成若干规则,可为制定编程规范提供依据,也可为代码审计或相关培训提供指导意见,旨在提升软件产品的可靠性、健壮性、可移植性以及可维护性,从而提升软件产品的综合安全性能。

C/C++ 安全规则集合

[外链图片转存中...(img-z2rxUD7I-1720615338134)]

Bjarne Stroustrup: “C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off.

  针对 C 和 C++ 语言,本文收录了 525 种需要重点关注的问题,可为制定编程规范提供依据,也可为代码审计以及相关培训提供指导意见,适用于桌面、服务端以及嵌入式等软件系统。
每个问题对应一条规则,每条规则可直接作为规范条款或审计检查点,本文是适用于不同应用场景的规则集合,读者可根据自身需求从中选取某个子集作为规范或审计依据,从而提高软件产品的安全性。

规则说明

规则按如下主题分为 17 个类别:

  1. Security:敏感信息防护与安全策略
  2. Resource:资源管理
  3. Precompile:预处理、宏、注释、文件
  4. Global:全局及命名空间作用域
  5. Type:类型设计与实现
  6. Declaration:声明
  7. Exception:异常
  8. Function:函数实现
  9. Control:流程控制
  10. Expression:表达式
  11. Literal:字面常量
  12. Cast:类型转换
  13. Buffer:缓冲区
  14. Pointer:指针
  15. Interruption:中断与信号处理
  16. Concurrency:异步与并发
  17. Style:样式与风格

每条规则包括:

  • 编号:规则在本文中的章节编号,以“R”开头,称为 Section-ID
  • 名称:用简练的短语描述违反规则的状况,以“ID_”开头,称为 Fault-ID
  • 标题:规则的定义
  • 说明:规则设立的原因、违反规则的后果、示例、改进建议、参照依据、参考资料等内容

如果违反规则,后果的严重程度分为:

  • Error:直接导致错误或形成安全漏洞
  • Warning:可导致错误或形成安全隐患
  • Suspicious:可疑的代码,需进一步排查
  • Suggestion:代码质量降低,应依照建议改进

规则的说明包含:

  • 示例:规则相关的示例代码,指明符合规则(Compliant)的和违反规则(Non-compliant)的情况
  • 相关:与当前规则有相关性的规则,可作为扩展阅读的线索
  • 依据:规则依照的 ISO/IEC 标准条目,C 规则以 ISO/IEC 9899:2011 为主,C++ 规则以 ISO/IEC 14882:2011 为主
  • 配置:某些规则的细节可灵活设置,审计工具可以此为参照实现定制化功能
  • 参考:规则参考的其他规范条目,如 C++ Core Guidelines、MISRA、SEI CERT Coding Standards 等,也可作为扩展阅读的线索

规则的相关性分为:

  • 特化:设规则 A 的特殊情况需要由规则 B 阐明,称规则 B 是规则 A 的特化
  • 泛化:与特化相反,称规则 A 是规则 B 的泛化
  • 相交:设两个规则针对不同的问题,但在内容上有一定的交集,称这两个规则相交

规则以“标准名称:版本 章节编号(段落编号)-性质”的格式引用标准,如“ISO/IEC 14882:2011 5.6(4)-undefined”,表示引用 C++11 标准的第 5 章第 6 节第 4 段说明的具有 undefined 性质的问题。

其中“性质”分为:

  • undefined:可使程序产生未定义的行为,这种行为造成的后果是不可预期的
  • unspecified:可使程序产生未声明的行为,这种行为由编译器或环境定义,具有随意性
  • implementation:可使程序产生由实现定义的行为,这种行为由编译器或环境定义,有明确的文档支持
  • deprecated:已被废弃的或不建议继续使用的编程方式

本文以 ISO/IEC 9899:2011、ISO/IEC 14882:2011 为主要依据,兼顾 C++17 以及历史标准,没有特殊说明的规则同时适用于 C 语言和 C++ 语言,只适用于某一种语言的规则会另有说明。

规则选取

本文是适用于不同应用场景的规则集合,读者可选取适合自己需求的规则。

指出某种错误的规则,如有“不可”、“不应”等字样的规则应尽量被选取,有“禁用”等字样的规则可能只适用于某一场景,可酌情选取。

如果将本文作为培训内容,为了全面理解各种场景下存在的问题,应选取全部规则。

规则列表

1. Security

2. Resource

3. Precompile

4. Global

5. Type

6. Declaration

7. Exception

8. Function

9. Control

10. Expression

11. Literal

12. Cast

13. Buffer

14. Pointer

15. Interruption

16. Concurrency

17. Style


1. Security

▌R1.1 敏感数据不可写入代码

ID_plainSensitiveInfo       :shield: security warning

代码中的敏感数据极易泄露,产品及相关运维、测试工具的代码均不可记录任何敏感数据。

示例:

/**
 * My name is Rabbit
 * My passphrase is Y2Fycm90         // Non-compliant
 */

#define PASSWORD "Y2Fycm90"          // Non-compliant

const char* passcode = "Y2Fycm90";   // Non-compliant

将密码等敏感数据写入代码是非常不安全的,即使例中 Y2Fycm90 是实际密码的某种变换,聪明的读者也会很快将其破解。

敏感数据的界定是产品设计的重要环节。对具有高可靠性要求的客户端软件,不建议保存任何敏感数据,对于必须保存敏感数据的软件系统,则需要落实安全的存储机制以及相关的评审与测试。

相关

ID_secretLeak

参考

CWE-259
CWE-798
SEI CERT MSC41-C


▌R1.2 敏感数据不可被系统外界感知

ID_secretLeak       :shield: security warning

敏感数据出入软件系统时需采用有效的保护措施。

示例:

void foo(User* u) {
    log("username: %s, password: %s", u->name, u->pw);   // Non-compliant
}

显然,将敏感数据直接输出到界面、日志或其他外界可感知的介质中是不安全的,需避免敏感数据的有意外传,除此之外,还需要落实具体的保护措施。

保护措施包括但不限于:

  • 避免用明文或弱加密方式传输敏感数据
  • 避免敏感数据从内存交换到外存
  • 避免敏感数据写入内存转储文件
  • 应具备反调试机制,使外界无法获得程序的内部数据
  • 应具备反注入机制,使外界无法篡改程序的行为

下面以 Windows 平台为例,给出阻止敏感数据从内存交换到外存的示例:

class SecretBuf {
    size_t len = 0;
    unsigned char* buf = nullptr;

public:
    SecretBuf(size_t size) {
        auto* tmp = (unsigned char*)VirtualAlloc(
            0, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE
        );
        if (VirtualLock(tmp, size)) {   // The key point
            buf = tmp;
            len = size;
        } else {
            VirtualFree(tmp, 0, MEM_RELEASE);
        }
    }

   ~SecretBuf() {
        SecureZeroMemory(buf, len);   // Clear the secret content
        VirtualUnlock(buf, len);
        VirtualFree(buf, 0, MEM_RELEASE);
        len = 0;
        buf = nullptr;
    }

    size_t size() const { return len; }
    unsigned char* ptr() { return buf; }
    const unsigned char* ptr() const { return buf; }
};

例中 SecretBuf 是一个缓冲区类,其申请的内存会被锁定在物理内存中,不会与外存交换,可在一定程度上防止其他进程的恶意嗅探,保障缓冲区内数据的安全。SecretBuf 在构造函数中通过 VirtualLock 锁定物理内存,在析构函数中通过 VirtualUnlock 解除锁定,解锁之前有必要清除数据,否则解锁之后残留数据仍有可能被交换到外存,进一步可参见 ID_unsafeCleanup。

SecretBuf 的使用方法如下:

void foo() {
    SecretBuf buf(256);
    if (buf.ptr()) {
        ....             // Do something secret using buf.ptr()
    } else {
        ....             // Handle memory error
    }
}

在 Linux 等系统中可参见如下有相似功能的接口:

int mlock(const void* addr, size_t len);     // In <sys/mman.h>
int munlock(const void* addr, size_t len);
int mlockall(int flags);
int munlockall(void);


相关

ID_unsafeCleanup

参考

CWE-528
CWE-591
SEI CERT MEM06-C


▌R1.3 敏感数据在使用后应被有效清理

ID_unsafeCleanup       :shield: security warning

及时清理不再使用的敏感数据是重要的安全措施,且应保证清理过程不会因为编译器的优化而失效。

程序会反复利用内存,敏感数据可能会残留在未初始化的对象或对象之间的填充数据中,如果被存储到磁盘或传输到网络就会造成敏感信息的泄露,可参见 ID_secretLeak 和 ID_ignorePaddingData 的进一步讨论。

示例:

void foo() {
    char password[8] = {};
    ....
    memset(password, 0, sizeof(password));  // Non-compliant
}

示例代码调用 memset 覆盖敏感数据以达到清理目的,然而保存敏感信息的 password 为局部数组且 memset 之后没有再被引用,根据相关标准,编译器可将 memset 过程去掉,使敏感数据没有得到有效清理。C11 提供了 memset_s 函数以避免这种问题,某些平台和库也提供了相关支持,如 SecureZeroMemory、explicit_bzero、OPENSSL_cleanse 等不会被优化掉的函数。

在 C++ 代码中,可用 volatile 限定相关数据以避免编译器的优化,再用 std::fill_n 等方法清理,如:

void foo() {
    char password[8] = {};
    ....
    volatile char  v_padding = 0;
    volatile char* v_address = password;
    std::fill_n(v_address, sizeof(password), v_padding);  // Compliant
}


相关

ID_secretLeak
ID_ignorePaddingData

依据

ISO/IEC 9899:1999 5.1.2.3(3)
ISO/IEC 9899:2011 5.1.2.3(4)
ISO/IEC 9899:2011 K.3.7.4.1

参考

CWE-14
CWE-226
CWE-244
CWE-733
SEI CERT MSC06-C


▌R1.4 公共成员或全局对象不应记录敏感数据

ID_sensitiveName       :shield: security warning

公共成员、全局对象可被外部代码引用,如果存有敏感数据则可能会被误用或窃取。

示例:

extern string password;   // Non-compliant

struct A {
    string username;
    string password;      // Non-compliant
};

至少应将相关成员改为 private:

class A {
public:
    ....                  // Interfaces for accessing passwords safely
private:
    string username;
    string password;      // Compliant
};

敏感数据最好对引用者完全隐藏,避免被恶意分析、复制或序列化。使数据与接口进一步分离,可参见“Pimpl idiom”等模式。

参考

CWE-766


▌R1.5 与内存空间布局相关的信息不可被外界感知

ID_addressExposure       :shield: security warning

函数、对象、缓冲区的地址以及相关内存区域的长度等信息不可被外界感知,否则会成为攻击者的线索。

示例:

int foo(int* p, int n) {
    if (n >= some_value) {
        log("buffer address: %p, size: %d", p, n);   // Non-compliant
    }
}

示例代码将缓冲区的地址和长度输出到日志是不安全的,这种代码多以调试为目的,不应将其编译到产品的正式版本中。

相关

ID_bufferOverflow

参考

CWE-200


▌R1.6 与网络地址相关的信息不应写入代码

ID_hardcodedIP       :shield: security warning

在代码中记录网络地址不利于维护和移植,也容易暴露产品的网络结构,属于安全隐患。

示例:

string host = "10.16.25.93";    // Non-compliant
foo("172.16.10.36:8080");       // Non-compliant
bar("https://192.168.73.90");   // Non-compliant

应从配置文件中获取地址,并配以加密措施:

MyConf cfg;
string host = cfg.host();   // Compliant
foo(cfg.port());            // Compliant
bar(cfg.url());             // Compliant

特殊的 IP 地址可不受本规则限制,如:

0.0.0.0
255.255.255.255
127.0.0.1-127.255.255.255


相关

ID_addressExposure


▌R1.7 预判用户输入造成的不良后果

ID_hijack       :shield: security warning

须对用户输入的脚本、路径、资源请求等信息进行预判,对产生不良后果的输入予以拒绝。

示例:

Result foo() {
    return sqlQuery(
        "select * from db where key='%s'", userInput()   // Non-compliant
    );
}

设 userInput 返回用户输入的字符串,sqlQuery 将用户输入替换格式化占位符后执行 SQL 语句,如果用户输入“xxx' or 'x'='x”一类的字符串则相当于执行的是“select * from db where key='xxx' or 'x'='x'”,一个恒为真的条件使 where 限制失效,造成所有数据被返回,这是一种常见的攻击方式,称为“SQL 注入(SQL injection)”,对于 XPath、XQuery、LDAP 等脚本均需考虑这种问题,应在执行前判断用户输入的安全性。

又如:

string bar() {
    return readFile(
        "/myhome/mydata/" + userInput()   // Non-compliant
    );
}

这段代码意在将用户输入的路径限制在 /myhome/mydata 目录下,然而这么做是不安全的,如果用户输入带有“../”这种相对路径,则仍可绕过限制,这也是一种常见的攻击方式,称为“路径遍历(directory traversal)”,应在读取文件之前判断路径的安全性。

注意,“用户输入”不单指人的手工输入,源自环境变量、配置文件以及其他软硬件的输入均在此范围内。

参考

CWE-23
CWE-73
CWE-89
CWE-943


▌R1.8 对资源设定合理的访问权限

ID_unlimitedAuthority       :shield: security warning

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C/C++面试必考必会 文章被收录于专栏

【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。

全部评论

相关推荐

02-22 20:28
重庆大学 Java
程序员牛肉:首先不要焦虑,你肯定是有希望的。 首先我觉得你得好好想一想自己想要什么。找不到开发岗就一定是失败的吗?那开发岗的35岁危机怎么说?因此无论是找工作还是考公我觉得你都需要慎重的想一想。但你一定要避开这样一个误区:“我是因为找不到工作所以不得不选择考公”。 千万不要这么想。你这个学历挺好的了,因此你投后端岗肯定是有面试机会的。有多少人简历写的再牛逼,直接连机筛简历都过不去有啥用?因此你先保持自信一点。 以你现在的水平的话,其实如果想要找到暑期实习就两个月:一个月做项目+深挖,并且不断的背八股。只要自己辛苦一点,五月份之前肯定是可以找到暑期实习的,你有点太过于高看大家之间的技术差距了。不要焦虑不要焦虑。 除此之外说回你这个简历内容的话,基本可以全丢了。如果想做后端,先踏踏实实做两个项目再说+背八股再说。如果想考公,那就直接备战考公。 但是但是就像我前面说的:你考公的理由可以是因为想追求稳定,想追求轻松。但唯独不能是因为觉得自己找不到工作。不能这么小瞧自己和自己的学历。
点赞 评论 收藏
分享
03-28 19:11
铜陵学院 C++
有礼貌的山羊追赶太阳:太典了,连笔试都没有开始就因为HC满了而结束了,而且还卡你不让你再投其他部门的。
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务