• CWE-591: Sensitive Data Storage in Improperly Locked Memory

Das Produkt speichert sensible Daten im nicht gesicherten Speicher oder in Speicher, der fehlerhaft gesichert wurde. Dies kann dazu führen, dass der virtuelle Speichermanager diese Daten auf die Auslagerungsdatei auf der Festplatte schreibt. Dies kann die Daten für externe Akteure zugänglicher machen.

CWE-591: Sensitive Data Storage in Improperly Locked Memory

CWE ID: 591
Name: Sensitive Data Storage in Improperly Locked Memory

Beschreibung

Das Produkt speichert sensible Daten im nicht gesicherten Speicher oder in Speicher, der fehlerhaft gesichert wurde. Dies kann dazu führen, dass der virtuelle Speichermanager diese Daten auf die Auslagerungsdatei auf der Festplatte schreibt. Dies kann die Daten für externe Akteure zugänglicher machen.

Erweiterte Beschreibung

Unter Windows-Systemen kann die VirtualLock-Funktion eine Speicherseite sperren, um sicherzustellen, dass sie im Speicher verbleibt und nicht auf die Festplatte ausgelagert wird. Bei älteren Windows-Versionen wie 95, 98 oder Me ist die VirtualLock()-Funktion jedoch lediglich ein Stub und bietet keinen Schutz. Unter POSIX-Systemen stellt der mlock()-Aufruf sicher, dass eine Seite im Speicher verbleibt, garantiert aber nicht, dass die Seite nicht in der Swap-Datei erscheint. Daher ist er ungeeignet als Schutzmechanismus für sensible Daten. Einige Plattformen, insbesondere Linux, geben zwar die Garantie, dass die Seite nicht ausgelagert wird, dies ist jedoch nicht standardisiert und nicht portabel. Auch mlock()-Aufrufe erfordern Supervisor-Privilegien. Die Rückgabewerte beider Aufrufe müssen überprüft werden, um sicherzustellen, dass die Sperroperation tatsächlich erfolgreich war.

Risikominderungsmaßnahmen

Maßnahme (Architecture and Design)

Effektivität: Unknown
Beschreibung: Okay, let’s break down how to identify data needing protection from swapping and select appropriate platform-appropriate mechanisms. Here’s a structured approach, combining identification and selection:

1. Identifying Data Requiring Protection from Swapping

The first step is to determine what data absolutely cannot be swapped to disk. This isn’t a blanket “everything” situation; it’s about identifying specific data categories. Consider these criteria:

  • Confidentiality: Data that, if compromised, would cause significant harm (e.g., encryption keys, personally identifiable information (PII), financial records, trade secrets, proprietary algorithms).
  • Integrity: Data where even a temporary modification (even if later corrected) could have severe consequences (e.g., critical system configuration files, database indexes, cryptographic hashes).
  • Availability: Data whose absence from memory would render a system or application unusable or severely impaired (e.g., active in-memory caches for critical services, real-time data streams).
  • Compliance/Regulatory Requirements: Certain regulations (e.g., PCI DSS, HIPAA) mandate specific data protection measures, which may include preventing swapping.
  • Performance Sensitivity: While not strictly a security concern, data frequently accessed and manipulated might benefit from being kept in memory to avoid performance bottlenecks caused by swapping. (This is often a secondary consideration after security.)

Examples of Data to Protect:

  • Encryption Keys: Absolutely critical. Compromise of keys renders all encrypted data vulnerable.
  • Active Database Connection Pools: If a connection pool is swapped, re-establishing connections can be slow and disruptive.
  • In-Memory Caches (e.g., Redis, Memcached): Swapping can lead to data loss and significant performance degradation.
  • Cryptographic Hashes/Signatures: Essential for verifying data integrity.
  • Sensitive Configuration Files: Files containing passwords, API keys, or other secrets.
  • Real-Time Data Streams: Data that must be processed immediately and cannot tolerate delays.

2. Selecting Platform-Appropriate Protection Mechanisms

Now, let’s look at how to protect this data, considering different operating systems. The goal is to prevent the OS from swapping the memory pages containing this data.

A. Windows

  • VirtualLock() (with caveats): As the original text mentions, VirtualLock() should prevent swapping. However, its reliability is questionable on older Windows versions. On modern Windows, it’s generally the preferred method.
    • Implementation: You need to allocate memory using VirtualAlloc() with MEM_LOCK and then call VirtualLock().
    • Limitations: Requires sufficient physical memory. If the system is under extreme memory pressure, even locked pages can be subject to pressure. Also, the amount of memory you can lock is limited by system resources.
  • Considerations: Use sparingly. Locking too much memory can negatively impact overall system stability. Monitor memory usage and system performance.

B. POSIX (Linux, macOS, Unix-like systems)

  • mlock(): The standard POSIX mechanism. It attempts to lock a region of memory into RAM.
    • Implementation: Use mlock() after allocating memory with malloc() or mmap().
    • Flags: The MCL_CURRENT flag is often used to lock the currently resident pages. MCL_FUTURE attempts to lock pages that will be brought into memory later.
  • madvise() with MADV_DONTDUMP: This suggests to the OS that the memory should not be included in core dumps. While not a guarantee against swapping, it’s a helpful complementary measure.
  • Kernel Memory Protection (Linux): More advanced techniques involving kernel modules or eBPF can provide finer-grained control over memory protection, but these are significantly more complex to implement.
  • Considerations: Like VirtualLock(), mlock() is subject to system memory constraints. The OS can still swap pages if absolutely necessary, although it will try to avoid it.

C. General Best Practices (Across Platforms)

  • Minimize the Amount of Data to Protect: Only lock the absolute minimum amount of memory required. Larger locked regions consume more resources and increase the risk of system instability.
  • Error Handling: Always check the return values of VirtualLock(), mlock(), and related functions. If the lock operation fails, handle the error gracefully (e.g., log the error, attempt to re-allocate memory, or take alternative action).
  • Memory Allocation Strategy: Consider using memory pools or other techniques to manage memory allocation and deallocation efficiently.
  • Regular Monitoring: Monitor system memory usage and performance to identify potential issues.
  • Security Audits: Regularly review your memory protection strategies to ensure they remain effective.
  • Documentation: Clearly document your memory protection mechanisms and the data they protect.

Example (Illustrative - Linux/C)

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <errno.h>

int main() {
    size_t data_size = 1024 * 1024; // 1MB
    void *data = malloc(data_size);

    if (data == NULL) {
        perror("malloc failed");
        return 1;
    }

    // Attempt to lock the memory
    if (mlock(data, data_size, MCL_CURRENT | MCL_FUTURE) == -1) {
        perror("mlock failed");
        free(data);
        return 1;
    }

    printf("Memory locked successfully.\n");

    // ... Use the data ...

    munlock(data); // Unlock the memory when done
    free(data);

    return 0;
}

Important Disclaimer: Memory protection is a complex topic. The information provided here is for general guidance only. Always consult the official documentation for your specific operating system and hardware platform. Thorough testing and security audits are essential to ensure the effectiveness of your memory protection strategies.

Maßnahme (Implementation)

Effektivität: Unknown
Beschreibung: You are absolutely correct to emphasize the importance of checking return values. It’s a critical, often overlooked, aspect of implementing memory protection. Let’s expand on that and provide more detailed examples of how to handle return values correctly.

Why Checking Return Values is Essential

  • Failure is Possible: Locking operations (like VirtualLock(), mlock()) are not guaranteed to succeed. System resources (physical memory, available address space) are finite. The OS might refuse to lock memory if it’s under severe pressure.
  • Silent Failures are Dangerous: If you blindly assume a lock operation succeeded, your application might continue operating under the false impression that the data is protected. This can lead to data compromise and unpredictable behavior.
  • Error Handling is Key: Proper error handling allows you to gracefully respond to failures, log the issue, and potentially take corrective action.

Detailed Examples of Return Value Checking

Let’s revisit the previous examples and incorporate robust return value checking.

1. Windows (using VirtualLock)

#include <windows.h>
#include <iostream>

int main() {
    size_t data_size = 1024 * 1024; // 1MB
    void* data = VirtualAlloc(NULL, data_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    if (data == NULL) {
        std::cerr << "VirtualAlloc failed: " << GetLastError() << std::endl;
        return 1;
    }

    // Attempt to lock the memory
    DWORD result = VirtualLock(data, data_size);
    if (result == 0) {
        std::cerr << "VirtualLock failed: " << GetLastError() << std::endl;
        VirtualFree(data, 0, MEM_RELEASE); // Clean up allocated memory
        return 1;
    }

    std::cout << "Memory locked successfully." << std::endl;

    // ... Use the data ...

    VirtualFree(data, 0, MEM_RELEASE); // Release the memory

    return 0;
}
  • GetLastError(): Windows functions often set the last error code if they fail. GetLastError() retrieves this code, providing more information about the cause of the failure.
  • VirtualFree(): If VirtualLock() fails, it’s crucial to release the memory allocated by VirtualAlloc() to avoid memory leaks.

2. Linux (using mlock)

#include <iostream>
#include <sys/mman.h>
#include <cerrno>

int main() {
    size_t data_size = 1024 * 1024; // 1MB
    void* data = malloc(data_size);

    if (data == NULL) {
        std::cerr << "malloc failed: " << strerror(errno) << std::endl;
        return 1;
    }

    // Attempt to lock the memory
    if (mlock(data, data_size, MCL_CURRENT | MCL_FUTURE) == -1) {
        std::cerr << "mlock failed: " << strerror(errno) << std::endl;
        free(data);
        return 1;
    }

    std::cout << "Memory locked successfully." << std::endl;

    // ... Use the data ...

    munlock(data); // Unlock the memory when done
    free(data);

    return 0;
}
  • strerror(errno): Linux functions often set the errno global variable to indicate an error. strerror(errno) converts this error code into a human-readable error message.
  • munlock(): Corresponding to mlock(), munlock() releases the locked memory. It’s essential to unlock memory when it’s no longer needed.

General Best Practices for Return Value Checking

  • Always Check: Never assume a locking operation succeeded. Always check the return value.
  • Handle Errors Gracefully: Don’t just print an error message and exit. Consider alternative actions, such as:
    • Trying to allocate memory in a different region.
    • Reducing the amount of data being locked.
    • Logging the error and continuing with a degraded mode of operation.
  • Clean Up Resources: If a locking operation fails after memory has been allocated, always release the allocated memory to prevent leaks.
  • Document Error Handling: Clearly document how your application handles locking failures.
  • Use Assertions (for Development): During development, use assertions to verify that locking operations succeed. This can help catch errors early. (e.g., assert(result != 0);)
  • Logging: Implement robust logging to record locking failures and other relevant information. This can be invaluable for debugging and troubleshooting.

By consistently incorporating these practices, you can significantly improve the reliability and security of your memory protection strategies. Remember that error handling is not an optional extra; it’s a fundamental requirement for robust software development.