Safeguarding Memory in Higher-Level Programming Languages

Consider an application written in a higher-level language like Python, NodeJS, or C#. This application must handle sensitive data such as banking credentials, credit card data, health information, or network passwords. The application developers have already hardened the application against malicious users and are confident that it is not vulnerable to database injections, account takeovers, or other remote critical-risk vulnerabilities. Furthermore, the application avoids storing sensitive data on the filesystem or sending it over network connections to other components.

Despite all of this hardening, however, the developers have overlooked a critical attack surface on this hypothetical application. It does not take any precautions to secure the data while storing it in memory. As a result of this lack of memory protections, a local attacker may be able to compromise the sensitive data by dumping the process memory.

Defending against a local attacker may seem like things have already reached the point of “too late.” This is not an unreasonable conclusion; a skilled local attacker with ample time will most likely be able to overcome any defensive control on the compromised machine. However, in practice, threat actors do not always have unlimited time before they are detected and locked out. Alternatively, they may lack the skills or resources necessary to overcome a particular control, even if that control is vulnerable in theory. For these reasons, local defenses still serve as adequate controls and are worth implementing when security is a top priority. In the event of a network breach, the memory protection techniques discussed below may buy the incident response team enough time to quarantine the affected device and lock out the actor before a damaging compromise occurs.

Complications to Designing Effective Memory Protections

Most higher-level programming languages are memory-managed and feature garbage collection (GC). GC is a memory recovery feature that automatically deallocates objects when they are no longer needed, which relieves the programmer of the burden of memory management.

Unfortunately, this also restricts the programmer from having total control over how and where their objects appear in memory. The GC process may involve moving data or copying it in memory without the application’s knowledge. Hence, a given data string may appear multiple times in memory and at unpredictable locations. Worst of all, these copies do not exist from the application’s perspective, so once they occur, the developer cannot natively design solutions to remove them from memory.

Adding to the complexity of this problem, most higher-level languages employ the concept of immutability. Immutable objects cannot truly be written to, and any operations that appear to modify them generate new copies of the data. This leaves the original copy in memory with no reference to reach it from the application. The `string` data type is immutable in most higher-level languages, including NodeJS, C#, and Python. Unfortunately, this data type often stores sensitive information in the real world.

The examples below address the above problems in a C# .NET application while minimizing the exposure of sensitive data in memory. Although we have chosen a .NET application for this paper, the same general solutions should apply to any higher-level programming language.

Starting Application

We begin with the following application:

```cs
using System;
namespace NaiveApplication
{
    class NaiveApplication
    {
        static void Main(string[] args)
        {
            string applicationSecret = "";
            ConsoleKeyInfo key;
            Console.Write("Enter secret data: ");
            do
            {
                key = Console.ReadKey(true);
                if (((int)key.Key) != 10)
                {
                    applicationSecret += key.KeyChar;
                    Console.Write("*");
                }
            } while (key.Key != ConsoleKey.Enter);
            Console.WriteLine("nnPress enter to "do" the application work.");
            // Figure 1 screenshot
            Console.ReadLine();
        }
    }
}
```

To simulate an “attack”, we used Procdump to dump the application’s memory at various stopping points (delineated in each example code with comments). The string `TOP SECRET DATA` served as a placeholder for sensitive data, and we inspected each memory dump for the placeholder string using HxD Hex Editor  As Figure 1 shows, the placeholder data is present in numerous memory locations. This includes a partial copy for each time the immutable string was “modified” inside the `for` loop.

Figure 1: An example Procdump output showing the sensitive data in the starting application’s memory.

Solutions

Use an Array

In C# (and most other programming languages), arrays are mutable and do not create additional copies when modified. A developer might try to reduce data exposure in memory by storing sensitive data in character arrays instead of strings and clearing out each array after use. This would prevent untrackable copies from stacking up due to immutability. The modified application would read as follows:

```cs
using System;
namespace CharacterArraySolution
{
    class CharacterArraySolution
    {
        static void Main(string[] args)
        {
            char[] applicationSecret = new char[64];
            ConsoleKeyInfo key;
            Console.Write("Enter secret data: ");
            int i = 0;
            do
            {
                key = Console.ReadKey(true);
                if (((int)key.Key) != 10)
                {
                    applicationSecret[i] = key.KeyChar;
                    i++;
                    Console.Write("*");
                }
            } while (key.Key != ConsoleKey.Enter);
            Console.WriteLine("nnPress enter to clear the secret.");
            Console.ReadLine();
            for (int j = 0; j < applicationSecret.Length; j++)
            {
                applicationSecret[j] = '';
            }
            Console.WriteLine("The secret is cleared. Press enter to exit.");
            // Figure 2 screenshot
            Console.ReadLine();
        }
    }
}
```

Unfortunately, the GC complicates this approach. If the GC compacts the block of memory containing the array, it may also create additional “copies” of the array in memory while leaving the original bytes as we see in Figure 2.

Figure 2: The data remains in memory even though the application cleared out the array.

However, this is a step in the right direction. The next iteration of the application will retain the ability to clear data from memory in a way that circumvents the GC.

Escaping the Garbage Collector

To circumvent the GC, the application must contain a portion of code that is not subject to memory management. The most reliable way to do this is to write an external library in C++ that handles all sensitive application data. C++ allows direct manipulation of memory and requires the programmer to manage the allocation of new objects and deallocation of old ones. For memory security, this will allow the application to erase all sensitive data from memory, ensuring that no additional copies from garbage collection or immutability remain.

To ensure this, the library must have the following properties:

  1. Dynamically-allocated arrays to store the data
  2. At least one method that explicitly clears out sensitive data
  3. One method for each operation that the main application must perform on the data.

The resulting DLL will be an “area” of the application that can safely hold sensitive data without worrying about garbage collection or immutability. The next iteration of the application exhibits this:

`DLLSimple.cpp`:
`DLLSimple.cpp`:
```cpp
#include "pch.h"
#include 
#include "DLLSimple.h"
class SecretData {
public:
    int id;
    char* dataArray;
    SecretData(int newId);
    bool addChar(char c);
    void clear();
private:
    int length;
    int maxSize = 64;
};
SecretData::SecretData(int newId) {
    dataArray = new char[maxSize];
    id = newId;
    length = 0;
}
bool SecretData::addChar(char c) {
    if (length >= maxSize)
        return false;
    *(dataArray + length) = c;
    length++;
    return true;
}
void SecretData::clear() {
    for (int i = 0; i < length; i++) {
        *(dataArray + i) = '';
    }
}
std::list<SecretData*> globalList;
int count = 0;
int createSecret()
{
    SecretData *newSecret = new SecretData(count);
    globalList.push_back(newSecret);
    return count++;
}
bool addCharToSecret(int id, char c) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++)
        if (id == (*it)->id)
            return (*it)->addChar(c);
    return false;
}
bool clearSecret(int id) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++) {
        if (id == (*it)->id) {
            (*it)->clear();
            delete (*it);
            globalList.remove((*it));
            return true;
        }
    }
    return false;
}
```
`DLLSimple.h`:
```cpp
#pragma once
#ifdef DLLSIMPLE_EXPORTS
#define DLLSIMPLE_API __declspec(dllexport)
#else
#define DLLSIMPLE_API __declspec(dllimport)
#endif
extern "C" DLLSIMPLE_API int createSecret();
extern "C" DLLSIMPLE_API bool addCharToSecret(int, char);
extern "C" DLLSIMPLE_API bool clearSecret(int);
```
Updated .NET Application:
```cs
using System;
using System.Runtime.InteropServices;
namespace DLLSolution
{
    class DLLSolution
    {
        [DllImport("DLLSimple")]
        private static extern int createSecret();
        [DllImport("DLLSimple")]
        private static extern bool addCharToSecret(int id, char c);
        [DllImport("DLLSimple")]
        private static extern bool clearSecret(int id);
        static void Main(string[] args)
        {
            int secretHandle = createSecret();
            ConsoleKeyInfo key;
            Console.Write("Enter secret data: ");
            int i = 0;
            do
            {
                key = Console.ReadKey(true);
                if (key.Key != ConsoleKey.Enter)
                {
                    addCharToSecret(secretHandle, key.KeyChar);
                    i++;
                    Console.Write("*");
                }
            } while (key.Key != ConsoleKey.Enter);
            Console.WriteLine("nnPress enter to clear the secret.");
            // Figure 3 screenshot
            Console.ReadLine();
            clearSecret(secretHandle);
            Console.WriteLine("The secret is cleared. Press enter to exit.");
            // Figure 4 screenshot
            Console.ReadLine();
        }
    }
}
```

A Note: If the reader is unfamiliar with writing custom DLLs and integrating them into a higher-level project, the following resources may be helpful:

In Figures 3 and 4 we see how this custom DLL loads “Top Secret Data” into memory while processing, but then allows it to clear.

Figure 3: Secret data stored in memory while being “processed”.

Figure 4: Once cleared, the placeholder string no longer appears in memory.

For most applications, circumventing the GC and clearing sensitive data arrays after use will serve as effective controls against a local attacker. However, if the application must keep the sensitive data in memory for long periods, an attacker can perform a timed-memory dump to extract the sensitive data. For such applications, this may constitute an unacceptable risk of exposure. The next iteration of the application will include protections against timed memory dumps.

Why not use a BSTR?

A class called a Binary String (BSTR), similar to a C-style character pointer, exists in .NET. Binary Strings are mutable and held in a “pinned” location of .NET memory, which is not subjected to the GC. Although a Binary String could safely handle sensitive data, we chose the DLL method to make this research more applicable to other programming languages that do not have the equivalent of a Binary String.

The Binary Strings approach also lacks a clean distinction in the code base between memory “safe” and “unsafe” code. If the secret data must pass through other functions or methods, ensuring the application never loads the data into an unpinned form is difficult. This is especially true when using external libraries. Writing a DLL provides a logistically separate application area to handle sensitive data safely.

Why not use memory pinning?

Binary Strings are not the only object that can be pinned in memory in .NET applications. Pinned instances of any objects can be created in .NET, removing them from the GC’s scope. In theory, there is nothing wrong with this approach, and in practice, it may be a better solution when dealing with complicated objects that cannot be easily converted into a set of arrays. However, the same note of caution mentioned for Binary Strings applies here.

Obfuscation

We now address the issue where an attacker can perform a memory dump during the timeframe that the application’s secret data exists in memory. The longer an application must hold secret data in memory, the more likely this type of attack is to succeed. Obfuscating the data in memory until the moment the user needs it would improve the previous DLL. A good obfuscation technique should render the obfuscated output indistinguishable from random bytes so it is difficult to spot out of a large memory dump. While not a silver bullet, this point warrants emphasis.

Several obfuscation techniques exist, and the correct choice depends on how the application must process the secret data.

DISCLAIMER: I used the `hash-library` C++ library for hashing in the example below. I chose this library because it was lightweight and easy to use. I did not research the cryptographical correctness of this library. Although I have no reason to believe this library is insecure, do not assume it is safe simply because I included it here. Proper cryptography is difficult to get right, which is why developers should always perform thorough research on cryptography libraries before including them in a production environment.

Hashing

Hashing is an ideal obfuscation technique for applications that only need to perform comparison operations on the secret data. By using a sparse hashing algorithm, the application can hash all secret data after loading it into the DLL and delete the cleartext data from memory. To perform a comparison, the application will hash the incoming data and compare the resulting hashes.

Note that salts are not an option with this approach, as using salts would cause the resulting hashes of identical cleartext inputs to be different. A universal salt for all inputs defeats the purpose of salting hashes.

The next iteration of the application will use a SHA256 hash to obfuscate all sensitive data. Once hashed, the cleartext will be completely removed from memory, as follows:

`DLLHashing.cpp`:

```cpp
#include "pch.h" 
#include <list>
#include "DLLHashing.h"
#include "sha256.h"
class SecretData {
public:
    int id;
    char* dataArray;
    bool dataArrayCleared;
    unsigned char* hashArray;
    bool hashArrayCleared;
    SecretData(int newId);
    bool addChar(char c);
    void obfuscate();
    void clearHashArray(); 
private:
    int length;
    int maxSize = 32;
    int hashSize = 32;
    void clearDataArray();
};
SecretData::SecretData(int newId) {
    dataArray = new char[maxSize];
    id = newId;
    length = 0;
    dataArrayCleared = false;
    hashArrayCleared = true;
}
bool SecretData::addChar(char c) {
    if (length >= maxSize)
        return false;
    *(dataArray + length) = c;
    length++;
    return true;
}
void SecretData::obfuscate()
{
    addChar('');
    hashArray = new unsigned char[hashSize+1];
    hashArrayCleared = false;
    SHA256 sha256;
    sha256(dataArray);
    sha256.getHash(hashArray);
    hashArray[hashSize] = '';
    clearDataArray();
}
void SecretData::clearDataArray() {
    if (dataArrayCleared)
        return;
    for (int i = 0; i < length; i++) {
        *(dataArray + i) = '';
    }
    delete dataArray;
    dataArrayCleared = true;
}
void SecretData::clearHashArray() {
    if (hashArrayCleared)
        return;
    for (int i = 0; i < hashSize; i++) {
        *(hashArray + i) = '';
    }
    delete hashArray;
    hashArrayCleared = true;
}
std::list<SecretData*> globalList;
int count = 0;
int createSecret()
{
    SecretData* newSecret = new SecretData(count);
    globalList.push_back(newSecret);
    return count++;
}
bool addCharToSecret(int id, char c) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++)
        if (id == (*it)->id)
            return (*it)->addChar(c);
    return false;
}
bool obfuscateSecret(int id) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++)
        if (id == (*it)->id) {
            (*it)->obfuscate();
            return true;
        }
    return false;
}
bool compareSecret(int id, char* input) {
    SHA256 sha256;
    const int hashSize = 32;
    unsigned char inputHash[hashSize];
    sha256(input);
    sha256.getHash(inputHash);
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++)
        if (id == (*it)->id) {
            for (int i = 0; i < hashSize; i++)
            if ((*it)->hashArray[i] != inputHash[i]) 
                return false;
            return true;
        }
    return false;
}
bool clearSecret(int id) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++) {
        if (id == (*it)->id) {
        (*it)->clearHashArray();
        delete (*it);
        globalList.remove((*it));
        return true;
        }
    }
    return false;
}
```
`DLLHashing.h` header file:
```
#pragma once
#ifdef DLLHASHING_EXPORTS
#define DLLHASHING_API __declspec(dllexport)
#else
#define DLLHASHING_API __declspec(dllimport)
#endif
extern "C" DLLHASHING_API int createSecret();
extern "C" DLLHASHING_API bool addCharToSecret(int, char);
extern "C" DLLHASHING_API bool clearSecret(int);
extern "C" DLLHASHING_API bool obfuscateSecret(int);
extern "C" DLLHASHING_API bool compareSecret(int, char*);
```
New .NET Application:
```cs
using System;
using System.Runtime.InteropServices;
namespace DLLSolution
{
    class DLLSolution
    {
        [DllImport("DLLHashing")]
        private static extern int createSecret();
        [DllImport("DLLHashing")]
        private static extern bool addCharToSecret(int id, char c);
        [DllImport("DLLHashing")]
        private static extern bool clearSecret(int id);
        [DllImport("DLLHashing")]
        private static extern bool obfuscateSecret(int id);
        [DllImport("DLLHashing")]
        private static extern bool compareSecret(int id, char[] input);
        static void Main(string[] args)
        {
            int secretHandle = createSecret();
            ConsoleKeyInfo key;
            Console.Write("Enter secret data: ");
            int i = 0;
            do
            {
                key = Console.ReadKey(true);
                if (key.Key != ConsoleKey.Enter)
                {
                    addCharToSecret(secretHandle, key.KeyChar);
                    i++;
                    Console.Write("*");
                }
            } while (key.Key != ConsoleKey.Enter);
            Console.WriteLine("nnPress enter to obfuscate the secret.");
            // Figure 5 screenshot
            Console.ReadLine(); 
            obfuscateSecret(secretHandle);
            Console.WriteLine("Press enter to compare the secrets.");
            // Figures 6 and 7 screenshots
            Console.ReadLine(); 
            char[] input = new char[64];
            Console.Write("Enter input data: ");
            i = 0;
            do
            {
                key = Console.ReadKey(true);
                if (key.Key != ConsoleKey.Enter)
                {
                    input[i] = key.KeyChar;
                    i++;
                    Console.Write(key.KeyChar);
                }
            } while (key.Key != ConsoleKey.Enter);
            bool res = compareSecret(secretHandle, input);
            Console.WriteLine("nComparison result: " + res);
            Console.WriteLine("Press enter to clear the secret.");
            Console.ReadLine();
            clearSecret(secretHandle);
            Console.WriteLine("The secret is cleared. Press enter to exit.");
            // Figure 8 screenshot
            Console.ReadLine(); 
        }
    }
}
```

Note that the above code assumes comparisons with the secret data that usually return `false`. Figures 5-8 walk us through the process of obfuscating under these conditions. For situations where the comparison operation would return `true` (e.g., logins), the `input` parameter should have similar protections.

Figure 5: The secret is loaded into memory but not obfuscated.

Figure 6: The secret is obfuscated. Its cleartext is no longer present.

Figure 7: However, the `sha256` hash of the secret (`6d217f6863def0c80c84eb0447b59d6c9ae0b0955d2b5079cb0012e0bdafe621`) is present.

Figure 8: Once the secret is cleared, the hash is also removed from memory.

The main advantage of using hashing for an obfuscation method is its irreversibility. An attacker accessing the application’s memory would need to crack each hash to recover the sensitive data.

The downside to using a hashing obfuscation is usability. Namely, the only operation the application can meaningfully perform with other input data is a comparison. If the application must perform selective reads or writes, then hashing would be inefficient and ineffective. The last iteration of the application will use an obfuscation technique that allows for more complicated operations.

Encryption

More complicated operations require a bidirectional obfuscation mechanism. In this iteration of our example application, the DLL will encrypt all sensitive data when not actively in use. Before an operation, the DLL will decrypt the information, operate, and re-encrypt the information.

Unlike with salting in the hashing scenario, an initialization vector is an option when developers have chosen encryption as their obfuscation technique. This is because all data will be decrypted before use, so there is no need for identical cleartexts to have identical ciphertexts.

Although a developer could use any secure encryption method, the following example uses the Microsoft Data Protection API (DPAPI):

`DLLEncryption.cpp`:
```cpp
#include "pch.h"
#include 
#include 
#include 
#include "DLLEncryption.h"
class SecretData {
public:
    int id;
    DATA_BLOB* dataBlob;
    SecretData(int newId);
    bool addChar(char c);
    bool obfuscate();
    bool deobfuscate();
    void clearDataBlob();
    void clearAndDelete();

private:
    int length;
    int maxSize = 32;
};
SecretData::SecretData(int newId) {
    dataBlob = new DATA_BLOB;
    dataBlob->pbData = new BYTE[maxSize];
    dataBlob->cbData = maxSize;
    id = newId;
    length = 0;
}
bool SecretData::addChar(char c) {
    if (length >= maxSize)
        return false;
    *(dataBlob->pbData + length) = c;
    length++;
    return true;
}
bool SecretData::obfuscate()
{
    if (CryptProtectMemory(dataBlob->pbData, dataBlob->cbData, CRYPTPROTECTMEMORY_SAME_PROCESS))
    {
        return true;
    }
    return false;
}
bool SecretData::deobfuscate()
{
    if (CryptUnprotectMemory(dataBlob->pbData, dataBlob->cbData, CRYPTPROTECTMEMORY_SAME_PROCESS))
    {
        return true;
    }
    return false;
}
void SecretData::clearDataBlob() {
    for (unsigned int i = 0; i < dataBlob->cbData; i++) {
        dataBlob->pbData[i] = '';
    }
}
void SecretData::clearAndDelete() {
    clearDataBlob();
    delete dataBlob->pbData;
    delete dataBlob;
}
std::list<SecretData*> globalList;
int count = 0;
int createSecret()
{
    SecretData* newSecret = new SecretData(count);
    globalList.push_back(newSecret);
    return count++;
}
bool addCharToSecret(int id, char c) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++)
        if (id == (*it)->id)
            return (*it)->addChar(c);
    return false;
}
bool obfuscateSecret(int id) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++)
        if (id == (*it)->id) {
            (*it)->obfuscate();
            return true;
        }
    return false;
}
bool doApplicationWork(int id) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++)
        if (id == (*it)->id) {
            (*it)->deobfuscate();
            Sleep(5000); // placeholder for work
            (*it)->obfuscate();
            return true;
        }
    return false;
}
bool clearSecret(int id) {
    for (std::list<SecretData*>::iterator it = globalList.begin(); it != globalList.end(); it++) {
        if (id == (*it)->id) {
            (*it)->clearAndDelete();
            delete (*it);
            globalList.remove((*it));
            return true;
        }
    }
    return false;
}
```
`DLLEncryption.h` header file:
```cpp
#pragma once
#pragma comment(lib, "crypt32.lib")
#ifdef DLLHASHING_EXPORTS
#define DLLHASHING_API __declspec(dllexport)
#else
#define DLLHASHING_API __declspec(dllimport)
#endif
extern "C" DLLHASHING_API int createSecret();
extern "C" DLLHASHING_API bool addCharToSecret(int, char);
extern "C" DLLHASHING_API bool clearSecret(int);
extern "C" DLLHASHING_API bool obfuscateSecret(int);
extern "C" DLLHASHING_API bool doApplicationWork(int);
New .NET Application:
```cs
using System;
using System.Runtime.InteropServices;
namespace DLLSolution
{
    class DLLSolution
    {
        [DllImport("DLLEncryption")]
        private static extern int createSecret();
        [DllImport("DLLEncryption")]
        private static extern bool addCharToSecret(int id, char c);
        [DllImport("DLLEncryption")]
        private static extern bool clearSecret(int id);
        [DllImport("DLLEncryption")]
        private static extern bool obfuscateSecret(int id);
        [DllImport("DLLEncryption")]
        private static extern bool doApplicationWork(int id);
        static void Main(string[] args)
        {
            int secretHandle = createSecret();
            ConsoleKeyInfo key;
            Console.Write("Enter secret data: ");
            int i = 0;
            do
            {
                key = Console.ReadKey(true);
                if (key.Key != ConsoleKey.Enter)
                {
                    addCharToSecret(secretHandle, key.KeyChar);
                    i++;
                    Console.Write("*");
                }
            } while (key.Key != ConsoleKey.Enter);
            Console.WriteLine("nnPress enter to obfuscate the secret.");
            // Figure 9 screenshot
            Console.ReadLine();
            obfuscateSecret(secretHandle);
            Console.WriteLine("Press enter to do the application "work"");
            // Figure 10 screenshot
            Console.ReadLine();
            doApplicationWork(secretHandle); // Figure 11 screenshot
            Console.WriteLine("Press enter to clear the secret.");
            Console.ReadLine();
            clearSecret(secretHandle);
            Console.WriteLine("The secret is cleared. Press enter to exit.");
            // Figure 12 screenshot
            Console.ReadLine();
        }
    }
}
```

Figures 9-12 show the obfuscation process that occurs via our example application.

Figure 9: The secret is loaded into memory before the encryption obfuscation occurs.

Figure 10: The secret is encrypted in memory. The cleartext secret does not appear anywhere in the memory dump.

Figure 11: The cleartext string reappears in memory while the application performs an operation.

Figure 12: No residual instances of the secret remain in memory after the application finishes using a secret.

The advantage of encryption is bi-directionality. This provides greater usability while still ensuring some measure of security. An attacker must find the application’s secret key before recovering sensitive data from a memory dump.

The downside of encryption is that the obfuscation’s security now rests on keeping the secret key away from the attacker. DPAPI on Windows partially mitigates this risk by punting the responsibility of key management to the OS, but securing the private key can be very difficult in other situations. If the cleartext private key is also in memory, bypassing the encryption is merely an extra step for the threat actor to perform. However, even this can be effective, as an attacker without advanced knowledge of the application’s internal functioning may not be able to spot the secret key in a large memory dump or distinguish encrypted data from random bytes.

A second downside to encryption is that the secret must be exposed in cleartext during a particular window of time, as it was in Figure 11.

Why not use a SecureString?

.NET includes a `SecureString` class that takes in a series of characters and stores them as an encrypted, pinned array. At first glance, this sounds ideal because:

However, despite these potential benefits, `SecureString` suffers from usability issues that often render it useless.  `SecureString` does not contain native properties or methods to decrypt the stored data. Instead, it must be marshaled into another object type (usually a Binary String). Unfortunately, this brings back the problem of holding cleartext secrets in memory. This is why Microsoft actively cautions against using`SecureString` in new projects.

Extra Mile – Rejecting Memory Access

The above controls will adequately protect the majority of desktop and web applications and increase the difficulty for a local attacker to recover sensitive data from memory. However, kernel-level protections may be worth considering for applications that require maximum security.

On Windows, an application can run as a “Protected Process” or “Protected Process Light” to (among other features) prevent local users and processes from accessing its memory. This is how many anti-malware services prevent tampering attempts from malicious processes. If a requesting process does not have a sufficient protection level, the OS will reject its request to access the target process memory, regardless of the requester’s user permissions. While not bulletproof, this can be a useful defensive layer to ward off attacks from privileged malware or local users.

The application must be signed by a valid Authenticode certificate from Windows or another trusted CA to run as a protected process, as Figure 13 shows. For more information, see the Microsoft Documentation.

Figure 13: An example Protected Process Light (Windows Defender). Note that the memory dump is rejected even with an administrator-level terminal.

Another kernel-level approach involves the Win32 API `OpenProcess` function. Invoking this function gains the user a handle for a target process, which they can use to access that process’s memory. By default, this function will fail for users with insufficient user-level privileges, but developers can also write a kernel driver to reject privileged calls to `OpenProcess`. Like Protected Processes, a trusted CA must sign all kernel drivers. Such a driver would have a similar effect of blocking memory dumps that local users or processes performed.

Conclusions

Securing an application’s memory is a complex problem with no complete solution, as a local attacker with sufficient time and resources will usually be able to overcome most local defensive measures. In practice, however, memory protections can serve as sufficient controls to deter an adversary until they are identified and removed. Memory protections are part of a defensive-in-depth strategy and can prevent a network breach from turning into a full asset compromise.

About the Authors

Catch the Latest

Catch our latest exploits, news, articles, and events.

Ready to Discuss Your Next Continuous Threat Exposure Management Initiative?

Praetorian’s Offense Security Experts are Ready to Answer Your Questions

0 Shares
Copy link