C++ 中的安全编码:避免缓冲区溢出和内存泄漏

enter image description here

软件漏洞可能带来严重威胁,包括数据泄露和整个系统被攻陷。编写安全代码对于降低这些风险和确保应用程序免受攻击至关重要。

在编程语言中,C++ 以其性能优势脱颖而出,这得益于低级内存管理。然而,这一特性使其天生容易受到缓冲区溢出和内存泄漏的影响。如果不加以解决,这些漏洞可能会导致应用程序崩溃、数据泄露,甚至未经授权的代码执行。

在探究缓冲区溢出和内存泄漏的缺陷之前,必须了解安全编码包含的内容以及为什么它在 C++ 中尤为重要。

安全编码可最大限度地减少软件漏洞,同时保护系统的机密性、完整性和可用性。对于 C++ 这样的语言来说,安全编码不是可选项,它经常用于系统软件、嵌入式系统、游戏开发和 AI 等高性能应用程序。它是必不可少的。

直接内存访问等使 C++ 功能强大的功能,如果不加以精心管理,也会带来安全风险。如果没有严格的保护措施,这些功能可能会导致内存损坏,并为漏洞利用打开方便之门。了解这些风险并采用安全编码实践对于充分利用 C++ 的优势而不损害系统安全性至关重要。

例如,如果不小心处理直接内存访问,这可能会导致内存损坏、缓冲区溢出和泄漏。

char buffer[10];  
strcpy(buffer, "This is a long string");  // No bounds checking — causes buffer overflow!
1
2
char buffer[10];  
strcpy(buffer, "This is a long string");  // No bounds checking — causes buffer overflow!

看一下上面的代码。如果输入超出了分配的缓冲区大小,它会覆盖相邻的内存,从而可能允许攻击者执行恶意代码。

看一下上面的代码。如果输入超出了分配的缓冲区大小,它会覆盖相邻的内存,从而可能允许攻击者执行恶意代码。

上述代码的输入字符串比缓冲区的大小(10 字节)长。由于strcpy()不检查边界,它会超出缓冲区的范围,从而破坏相邻的内存。这可能会导致程序崩溃或允许攻击者注入恶意代码。

缓冲区溢出的一些真实示例包括1988 年的 Morris 蠕虫。这是最早的互联网蠕虫之一,它利用了 Unix 函数中的缓冲区溢出漏洞gets()。通过反复感染机器,它导致了大范围的网络减速。

另一个场景是2014 年的 Heartbleed 漏洞。OpenSSL 的 Heartbeat 扩展中的这个漏洞允许攻击者从内存中读取敏感数据。未经检查的缓冲区大小导致了此漏洞,从而导致密码、私钥和敏感用户数据的泄露。

那么,我们如何缓解缓冲区溢出漏洞呢?有几种方法可以保护我们的系统免受这一巨大缺陷的影响。

其中之一是使用更安全的字符串函数。strcpy()使用而不是strncpy()来指定缓冲区限制。请参阅下面的代码。

#include <iostream>
#include <cstring>

void safeFunction(const char* userInput) {
    char buffer[10];  
    strncpy(buffer, userInput, sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0'; // Ensure null termination
    std::cout << "Buffer content: " << buffer << std::endl;
}

int main() {
    const char* input = "This is safe!";
    safeFunction(input);
    return 0;
}

我们还可以使用有界输入函数。避免使用gets()-fgets()而是使用。

char input[50];
fgets(input, sizeof(input), stdin);

char input[50];
fgets(input, sizeof(input), stdin);

另一种选择是使用现代 C++ 特性,例如std::string。自动管理内存,而不是原始字符数组``` std::string。 std::string userInput; std::cin >> userInput; // No overflow risk

std::string userInput; std::cin >> userInput; // No overflow risk

您还可以启用编译器保护,因为现代编译器提供了针对缓冲区溢出的保护。

g++ -fstack-protector-strong -o secure_program main.cpp g++ -fstack-protector-strong -o secure_program main.cpp

最后,AddressSanitizer可用于检测缓冲区溢出。

g++ -fsanitize=address -g main.cpp -o main./maing++ -fsanitize=address -g main.cpp -o main./main

上述代码将在运行时检测溢出。

接下来我们来看看内存泄漏。

当程序动态分配内存但从不释放时,就会发生内存泄漏。随着时间的推移,程序会消耗越来越多的内存,导致性能下降、系统变慢和应用程序崩溃。

这通常是由于开发人员忘记释放分配的内存而导致的。请记住,C++ 没有像 Python 或 Java 那样的自动垃圾收集功能,因此忘记删除动态分配的内存会导致泄漏。请参阅下面的代码。

include

void memoryLeak() { int* ptr = new int(100); // Dynamically allocated integer std::cout << "Allocated memory: " << *ptr << std::endl; // No deletion statement }

int main() { while (true) { memoryLeak(); // Continuous memory allocation without deallocation } return 0; }


因此,为了让您了解正在发生的事情,每次调用都会memoryLeak()分配新内存。由于delete ptr;缺少,程序会无限期地消耗内存,因为指针不会被删除,从而导致内存耗尽,最终导致系统速度变慢或崩溃。 要解决这个问题,我们要做的就是删除指针。

void fixedFunction() { int* ptr = new int(100); std::cout << "Allocated memory: " << *ptr << std::endl; delete ptr; // Properly free the memory }


另一个常见原因是在循环内分配内存但没有在下一次迭代之前释放它。

include

int main() { for (int i = 0; i < 100000; i++) { int* arr = new int[1000]; // Memory allocated in each iteration // Forgot to delete the array } return 0; }


为了解决这个问题,必须在下一次迭代之前删除该数组。

for (int i = 0; i < 100000; i++) { int* arr = new int[1000]; delete[] arr; // Properly freeing allocated memory }


在其他情况下,内存泄漏在返回指针的函数中很常见。当函数返回动态分配的内存时,调用者负责释放它。

int* getMemory() { int* ptr = new int(42); return ptr; // Caller must delete this }

int main() { int* myPtr = getMemory(); std::cout << *myPtr << std::endl; // Forgot to delete myPtr; }

最后,动态分配内存但不在析构函数中释放内存的类可能会导致内存泄漏。

class LeakyClass { private: int* data;

public: LeakyClass() { data = new int[100]; // Allocating memory } // Destructor is missing! Memory is never freed };

int main() { LeakyClass* obj = new LeakyClass(); delete obj; // Destructor doesn’t free memory! }


为了修复这个问题,添加一个析构函数。

class FixedClass { private: int* data;

public: FixedClass() { data = new int[100]; } ~FixedClass() { delete[] data; } // Properly freeing memory };

为了解决这个问题,我们可以使用一些技术来保护我们的应用程序免受内存泄漏的影响。让我们探索其中的一些。

1. 始终释放动态分配的内存。每个“新建”都必须有相应的“删除”。

int* ptr = new int(5); // No delete, leading to a memory leak! int* ptr = new int(5); delete ptr; // Properly freed

int* ptr = new int(5); // No delete, leading to a memory leak! int* ptr = new int(5); delete ptr; // Properly freed

2. 优先使用std::vector而不是原始数组。new int[100]使用 而不是 ,std::vector它可以自动管理内存。

// instead of this int* arr = new int[100]; // Forgot to delete[] arr;

// use this std::vector arr(100); // No need to manually manage memory

// instead of this
int* arr = new int[100];
// Forgot to delete[] arr;

// use this
std::vector<int> arr(100);  // No need to manually manage memory

3.std::unique_ptr用于单个对象。std::unique_ptr automatically frees memory when it goes out of scope.

#include <memory>

void smartPointerExample() {
    std::unique_ptr<int> smartPtr = std::make_unique<int>(42);
    std::cout << *smartPtr << std::endl;  // No need for manual delete
}  // Memory is automatically freed here

4.When multiple objects share ownership of dynamically allocated memory, use std::shared_ptr`。

#include <memory>

int main() {
    std::shared_ptr<int> shared1 = std::make_shared<int>(42);
    std::shared_ptr<int> shared2 = shared1;  // Both share ownership
    std::cout << *shared1 << ", " << *shared2 << std::endl;
}  // Memory is freed automatically when last owner is destroyed

最后,使用 RAII 习语(“资源获取即初始化”),确保在对象的生命周期内分配和释放内存。

#include <vector>

void useVector() {
    std::vector<int> numbers(100);  // Memory is managed automatically
}

除了使用这些预防方法外,在开发过程中检测泄漏也很重要。

对于 Mac/Linux 用户,在执行期间使用Valgrind 。

valgrind --leak-check=full ./main

对于 Windows 用户,使用Visual Studio 内存泄漏检测。

#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>

int main() {
    _CrtDumpMemoryLeaks();  // Check for leaks at program exit
}

C++ 的优势是一把双刃剑,如果管理不善,可能会造成致命后果。缓冲区溢出和内存泄漏是严重的安全漏洞,可能导致系统崩溃、性能下降甚至安全漏洞。攻击者可以利用缓冲区溢出执行任意代码,而内存泄漏会逐渐消耗系统资源,导致不稳定。通过遵循安全编码实践(例如边界检查、使用智能指针和应用 RAII 原则),开发人员可以安全高效地利用 C++ 的强大功能。编写安全的 C++ 代码不仅是为了避免错误,还为了构建经得起时间考验的弹性高性能软件。

评论