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] = 'Share via: