1.安全编程的基本原则
安全编程的基本原则包括:
- 最小权限:程序只拥有所需的最小权限。
- 输入验证:对所有用户输入进行严格验证。
- 错误处理:正确处理错误和异常情况。
- 资源管理:及时释放资源,避免内存泄漏。
- 数据隐私:保护用户数据的隐私和安全。
2.常见安全漏洞
常见的安全漏洞包括:
- 缓冲区溢出:由于不正确的内存管理,程序可能会超出缓冲区的边界,导致数据泄漏或程序崩溃。
- 格式字符串攻击:攻击者可以通过格式字符串函数(如printf和sprintf)注入恶意代码,导致程序执行恶意命令。
- 错误处理漏洞:如果程序没有正确处理错误和异常情况,攻击者可能利用这些漏洞进行攻击。
- 权限提升:攻击者可能通过恶意文件或恶意程序获得更高的权限,从而控制系统。
3.缓冲区溢出
3.1缓冲区溢出的防御
缓冲区溢出的防御措施包括:
- 使用动态数组:动态数组可以根据实际需求自动调整大小,避免缓冲区溢出。
- 使用STL容器:STL容器(如vector和string)提供了内存管理功能,可以防止缓冲区溢出。
- 使用内存分配器:内存分配器可以自动检测缓冲区溢出,并释放恶意数据。
3.2示例

在构造函数体中,使用new关键字创建了一个名为signalBase的EquipBase类型的新对象。这个对象是通过动态分配内存来创建的,因此需要在析构函数中显式释放内存。在GetSignal类的析构函数中,使用delete关键字释放了signalBase对象所占用的内存。这是因为signalBase对象是通过new关键字动态分配内存创建的,如果不显式释放内存,就会造成内存泄漏。
动态分配内存:
动态分配的内存不会在声明它的变量离开作用域时自动释放,而是需要程序员显式地使用delete(对于单个对象)或delete[](对于数组)来释放。如果忘记释放动态分配的内存,就会造成内存泄漏。
动态内存分配通常用于以下情况:
当你需要的内存大小在编译时未知,或者直到程序运行时才能确定。
当你处理的数据量非常大,以至于无法在栈上分配或者不适合在栈上分配。
当你需要创建对象的副本,但又希望控制对象的生命周期时。
1.使用new操作符:这是最常见的动态内存分配方式。当你使用new关键字时,你实际上是在请求堆(heap)上的一块内存,并返回一个指向该内存的指针。
例如:int* ptr = new int; 这会在堆上分配一个整数大小的内存,并返回一个指向该内存的指针。释放:delete ptr;
2.使用new[]操作符:当你需要分配一个数组时,可以使用new[]操作符。
例如:int* arr = new int[10]; 这会在堆上分配一个可以存储10个整数的数组,并返回一个指向数组首元素的指针。释放:delete[] arr;
3.使用标准库容器(如std::vector、std::list、std::map等):这些容器会根据需要动态地分配和重新分配内存以存储其元素。分配和释放:通常不需要手动管理。
4.使用malloc、calloc、realloc函数(来自C标准库):虽然这些函数源自C语言,但在C++中仍然可以使用。它们通过stdlib.h(或C++中的cstdlib)提供。
例如:int* ptr = (int*)malloc(sizeof(int)); 这会在堆上分配一个整数大小的内存。释放:free(ptr);
5.使用std::make_shared和std::make_unique(C++11及以后):这些函数用于创建一个管理动态分配对象生命周期的智能指针。这些函数创建的智能指针会自动管理内存。当最后一个拥有者超出作用域时,内存会被自动释放。
例如:std::unique_ptr<int> ptr = std::make_unique<int>(10); 这会创建一个unique_ptr,它拥有一个在堆上动态分配的整数。释放:自动,不需要手动释放。
6.使用std::make_shared和std::make_unique:这些函数创建的智能指针会自动管理内存。当最后一个拥有者超出作用域时,内存会被自动释放。
例如:分配:std::shared_ptr<int> ptr = std::make_shared<int>(10);释放:自动,不需要手动释放。
(补充:设置指针为nullptr:释放内存后,将signalBase指针设置为nullptr,可以避免悬空指针(dangling pointer)的问题。悬空指针是指指向已经被释放内存的指针,如果继续使用这样的指针,可能会导致程序崩溃或未定义行为。其次可以防止重复释放:将signalBase指针设置为nullptr可以避免在其他代码路径中意外释放已经被释放的内存,从而导致程序崩溃。)
4.格式字符串攻击
4.1格式字符串攻击的防御
格式字符串攻击的防御措施包括:
- 使用非格式字符串函数:非格式字符串函数(如snprintf和sprintf_s)可以防止格式字符串攻击。
- 使用安全函数库:安全函数库(如Microsoft Security Development Lifecycle)提供了防御格式字符串攻击的工具和技术。
4.2不安全的代码示例

在这个例子中,userInput是一个字符串,它包含了%s、%x和%n三个格式化指令。这些指令的作用如下:
%s:打印出一个字符串。
%x:打印出一个十六进制数。
%n:将到目前为止打印的字符数存储在后续的整数参数中。
由于printf函数期望在格式字符串之后有更多的参数来匹配这些格式化指令,如果没有提供足够的参数,它就会尝试从相邻的内存位置读取数据。这可能导致未定义的行为,包括打印出敏感数据或导致程序崩溃。
4.3安全的代码示例

在C++中,防御格式字符串攻击的方法与C语言类似,但C++提供了一些额外的特性和库,可以帮助我们更安全地处理字符串。以下是一些在C++中防御格式字符串攻击的方法:
1. 使用std::snprintf而不是sprintf:,std::snprintf函数允许指定缓冲区的最大长度,这样可以防止写入超出缓冲区的数据。如上所示
这里user_input是一个std::string对象,c_str()方法返回一个指向以空字符终止的字符数组的指针。
(补充:int snprintf ( char * str, size_t size, const char * format, ... );
str -- 目标字符串,用于存储格式化后的字符串的字符数组的指针。
size -- 字符数组的大小。
format -- 格式化字符串。
... -- 可变参数,可变数量的参数根据 format 中的格式化指令进行格式化。)
2. 使用C++标准库中的字符串类:
C++标准库提供了std::string和std::stringstream等类,它们可以帮助我们避免直接操作字符数组,从而减少缓冲区溢出的风险。例如,使用std::stringstream来构建格式化的字符串:

std::stringstream允许将字符串以流的形式处理,就像对待标准输入输出流(std::cin 和 std::cout)一样。std::stringstream 可以用来格式化字符串,并且完全避免了缓冲区溢出的风险,因为它会自动根据需要分配内存。
3. 使用std::format(C++20):
如果使用的是支持C++20标准的编译器,那么可以用std::format,这是一个类型安全且异常安全的字符串格式化库。例如:

4. 使用std::ostringstream代替sprintf:
std::ostringstream是标准库中的一个类模板,可以用来格式化字符串,而不需要担心缓冲区溢出的问题。例如:

5. 输入验证:
在将任何用户输入传递给格式化函数之前,始终进行严格的输入验证和清理,确保输入符合预期的格式。
6. 使用C++的异常处理:
C++的异常处理机制可以用来处理在格式化字符串时可能发生的错误情况,例如缓冲区溢出。
7. 代码审计和静态分析:
使用静态分析工具来检测代码中的潜在格式字符串漏洞,并进行定期的代码审计。
通过采用这些方法,C++开发者可以显著降低格式字符串攻击的风险,并提高软件的安全性。例如,Clang Analyzer、Coverity 和 Fortify Source Code Analyzer 都是可以识别格式字符串漏洞的工具。
5.错误处理漏洞
5.1错误处理漏洞的防御
错误处理漏洞的防御措施包括:
- 使用异常处理:异常处理可以捕获和处理错误和异常情况,防止攻击者利用这些漏洞。
- 使用错误代码:错误代码可以提供详细的错误信息,帮助开发者及时发现和修复错误。
5.2代码示例
1.使用异常处理
在 C++ 中,异常处理是通过 try、catch、throw 和 std::exception 等关键字和类来实现的。通过使用异常处理,程序可以在发生错误时捕获异常,并提供相应的处理逻辑,而不是让程序崩溃或产生不可预测的行为。

在这个示例中,我们使用 try 块来包围可能抛出异常的代码。如果 user_input 是空的,我们使用 throw 关键字抛出一个 std::runtime_error 异常。然后,我们使用 catch 块来捕获并处理这个异常。catch 块可以指定特定的异常类型,也可以捕获所有其他类型的异常。
2.使用错误代码
错误代码是一种提供错误详细信息的方法。在 C++ 中,通常通过返回特定的错误代码或使用错误码枚举来实现。此外,可以使用 std::error_code 类(在 <system_error> 头文件中定义)来提供更多关于错误的信息。

在这个示例中,我们定义了一个 MyErrorCode 枚举来表示不同的错误代码,并在 ProcessUserInput 函数中使用 throw 抛出一个 std::system_error 异常,如果输入为空,则传递 MyErrorCode::EmptyInput 作为错误代码。在 main 函数中,我们捕获这个异常,并使用 e.what() 和 e.code() 来获取错误描述和错误代码。
6.权限提升
6.1权限提升的防御
权限提升的防御措施包括:
- 使用访问控制:访问控制可以限制程序的权限,防止攻击者获得更高的权限。
- 使用安全策略:安全策略可以定义程序的权限和行为,防止攻击者篡改程序。
6.2使用访问控制示例
1.操作系统层面
可以使用 chmod 命令限制执行文件的权限,防止未授权的访问:
chmod 700 /path/to/executable
这条命令将执行文件的权限设置为只有所有者可以读取、写入和执行。
2.应用程序层面
在 Web 应用程序中,可以使用角色基于的访问控制(RBAC)来限制用户对特定功能的访问:

在这个示例中,我们定义了一个简单的权限检查函数 HasPermission,它检查给定的用户角色是否具有执行特定操作的权限。
6.3使用安全策略示例
1.操作系统层面
在 Windows 系统中,可以使用安全策略来限制程序的权限:
# 使用 PowerShell 设置安全策略

这条命令设置了当前用户的执行策略为受限,这意味着只有签名的脚本才能被执行。
Set-ExecutionPolicy:这是一个PowerShell的cmdlet(命令),用于设置执行策略。
-Scope CurrentUser:这个参数指定了策略的作用范围。CurrentUser表示这个策略只影响当前用户,而不是整个系统。
-ExecutionPolicy Restricted:这个参数指定了执行策略的类型。Restricted是其中一种策略,表示不允许执行任何脚本,无论是本地还是远程的。只有经过数字签名的脚本和Windows PowerShell模块才能被执行。
所以,当在PowerShell中输入这条命令并执行后,它会将当前用户的执行策略设置为Restricted。这意味着,除非你运行的脚本是由可信的发布者签名的,否则PowerShell将不允许执行任何脚本。这是一种提高系统安全性的措施,可以防止恶意脚本的执行。
2.应用程序层面
在软件开发中,可以实施代码签名和安全编译策略,确保软件在分发和运行时的完整性:

在这个示例中,我们在 main 函数开始时调用 verifyCodeSignature 函数来检查代码签名。如果签名无效,程序将终止执行。
(补充:代码签名是一种安全机制,用于验证软件或代码的来源和完整性。它通常涉及数字签名技术,确保代码自签名以来未被篡改,并且确实来自声称的开发者或组织。以下是代码签名的一些关键组成部分:
数字证书:代码签名通常使用数字证书,这是一种由可信的证书颁发机构(CA)颁发的电子文档,用于证明软件的发布者身份。数字证书包含公钥、证书持有者信息(如公司名称和位置)、证书颁发者信息、有效期等。
哈希算法:在签名过程中,使用哈希算法(如SHA-256)对代码生成一个唯一的哈希值(哈希摘要)。这个哈希值随后与开发者的私钥一起加密,形成数字签名。
私钥和公钥:数字签名使用私钥加密,确保只有持有相应公钥的人才能解密和验证签名。公钥通常与数字证书一起分发,或者包含在软件包中。
签名验证:当用户下载或运行软件时,系统可以使用开发者的公钥来解密数字签名,并重新计算代码的哈希值。如果解密后的哈希值与重新计算的哈希值匹配,并且证书有效,那么签名被认为是有效的。
在实际应用中,代码签名的实现和验证通常由操作系统和安全软件处理,而不是在应用程序代码中直接处理。
例如:
Windows:使用 Authenticode 技术,可以通过 Microsoft 的 CryptoAPI 来验证签名。
macOS 和 iOS:使用 Apple 的代码签名基础设施,可以通过 codesign 命令行工具来验证签名。
Linux:可以使用如 rpm 或 deb 包管理器的签名验证工具。)
参考博客
《C++安全编程:防范常见的安全漏洞与攻击》
原文链接:https://blog.csdn.net/universsky2015/article/details/136011086
2451

被折叠的 条评论
为什么被折叠?



