RAM pamięta wszystko: niewidoczne zagrożenie w aplikacjach

Karol Mazurek

Aplikacje desktopowe działające na Windowsie, Linuksie i macOS-ie często przetwarzają wrażliwe informacje, takie jak hasła czy klucze API. Choć wielu deweloperom może się wydawać, że dane te są usuwane po użyciu, w rzeczywistości zazwyczaj pozostają w pamięci dłużej, niż by się tego spodziewali. Ten problem związany z trwałością danych w pamięci to subtelna, wieloplatformowa podatność, którą nawet doświadczeni programiści potrafią przeoczyć.

W tym artykule wyjaśnię, dlaczego i kiedy jest to zagrożenie oraz jakie istnieją sposoby jego ograniczenia. Miłej lektury!

Jak systemy operacyjne zarządzają pamięcią

System wykorzystuje RAM do przechowywania danych podczas wykonywania programu. Dane te nie są natychmiast usuwane po ich użyciu – pozostają w pamięci do momentu, aż system nadpisze dany obszar (lub aż urządzenie zostanie wyłączone). Oznacza to, że wrażliwe informacje mogą przetrwać w RAM-ie, a dostęp do nich może uzyskać każdy, kto ma wystarczające uprawnienia do przeglądania pamięci.

  • Windows: Pamięć może być odczytywana za pomocą interfejsów debugowania, które są pobłażliwe.
  • Linux: Podobnie jak powyżej, plus możliwość odczytu z /proc/<pid>/mem, choć dostęp do tego pliku mają zazwyczaj tylko użytkownicy z uprawnieniami roota.
  • macOS: Hardened Runtime wprowadza dodatkowe zabezpieczenia – ale tylko jeśli aplikacja z niego korzysta.

Warto jednak zaznaczyć, że są to jedynie środki łagodzące skutki problemu, a nie jego rozwiązania.

Odpowiedzialność dewelopera

Najważniejsze jest to, że żaden system operacyjny nie usuwa wrażliwych danych z pamięci aplikacji automatycznie. Zadbanie o to, aby odpowiednio je zabezpieczyć stanowi odpowiedzialność dewelopera. Wysokopoziomowe języki programowania i wykorzystywane przez nie frameworki oferują różne API umożliwiające bezpieczne czyszczenie danych z pamięci. Należy pamiętać, że nawet jeśli zmienna wychodzi poza zakres lub zostaje zdealokowana, jej zawartość nadal może pozostać w pamięci, dopóki nie zostanie nadpisana. Właśnie dlatego istnieją specjalne funkcje do ich czyszczenia. Co więcej, nawet w przypadku aplikacji napisanych w języku C, kompilator może zignorować (zoptymalizować) próbę wyczyszczenia pamięci — więcej na ten temat w artykule: Locking the Vault: The Risks of Memory Data Residue.

Poniżej znajdują się przykłady zabezpieczania wrażliwych danych w różnych językach programowania. Jak się jednak przekonamy, czasami nie jest to w ogóle możliwe.

C

Poniższy program alokuje pamięć do przechowania hasła i czyści ją po użyciu, zapewniając, że dane nie pozostają w pamięci:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

// Securely wipe memory to prevent sensitive data recovery
static void secure_wipe(void *target, size_t bytes) {
    volatile unsigned char *ptr = target;
    while (bytes--) *ptr++ = 0;
}

void demo_clear_memory() {
    const size_t BUFFER_SIZE = 32;
    char *password_buffer = malloc(BUFFER_SIZE);
    if (!password_buffer) return;

    // Get password securely without echo
    const char *user_input = getpass("Password: ");
    if (user_input) {
        // Ensure password fits in buffer with room for null
        size_t password_length = strlen(user_input) % (BUFFER_SIZE - 1);
        
        // Copy password to secured buffer
        memcpy(password_buffer, user_input, password_length);
        password_buffer[password_length] = 0;
        
        // Wipe original input to prevent leaks
        secure_wipe((void*)user_input, password_length);
        
        // Show masked password
        printf("Stored: %.*s\n", (int)password_length, "********");
    }
    
    // Clean up sensitive data
    secure_wipe(password_buffer, BUFFER_SIZE);
    free(password_buffer);
    printf("Cleared. Enter to exit...");
    getchar();
}

int main() {
    demo_clear_memory();
    return 0;
}Code language: PHP (php)

Po skompilowaniu i uruchomieniu programu możemy zaobserwować, że hasło zostało usunięte z pamięci:

To przykład poprawnego podejścia. Ale bądźmy szczerzy – kto dziś pisze aplikacje w czystym C?

C#

W C# możemy użyć typu SecureString jako kontenera na dane wrażliwe. Takie podejście szyfruje hasło w pamięci oraz zeruje zawartość bufora po zwolnieniu zasobu:

using System.Security;

class Program
{
    static void Main()
    {
        Console.Write("Enter password: ");
        using (SecureString securePwd = new SecureString())
        {
            ConsoleKeyInfo key;
            while ((key = Console.ReadKey(true)).Key != ConsoleKey.Enter)
            {
                securePwd.AppendChar(key.KeyChar);
                Console.Write("*");
            }
            Console.WriteLine($"\nPassword length: {securePwd.Length}");
            Console.WriteLine("Press Enter to exit...");
            Console.ReadLine();
        }
    }
}Code language: JavaScript (javascript)

Jak widać, również w tym przypadku hasło nie zostało odnalezione w zrzucie pamięci:

Kod jest też krótszy.

SWIFT

Swift nie oferuje wbudowanego mechanizmu bezpiecznej pamięci jak SecureString w C#. Nie ma gotowego API, a nawet próba wyczyszczenia bufora przy pomocy memset napotyka problemy:

import Foundation

print("Enter password: ", terminator: "")
if let cString = getpass("") {
    let password = String(cString: cString)
    print("\nPassword length: \(password.count)")

    // Zero out the buffer after use
    memset(UnsafeMutableRawPointer(mutating: cString), 0, strlen(cString))

    print("Press Enter to exit...")
    _ = readLine()
}Code language: JavaScript (javascript)

Jak pokazano poniżej, hasło i tak wyciekło do pamięci. Dzieje się tak z powodu konwersji String(cString: cString) – dane zostają skopiowane do innego miejsca w pamięci i nie zostają poprawnie wyczyszczone:

Znalazłem obejście, w którym przechowuję hasło w ręcznie zarządzanym buforze:

import Foundation

print("Enter password: ", terminator: "")
if let cString = getpass("") {
    let length = strlen(cString)
    // Allocate a buffer
    let buffer = UnsafeMutablePointer<CChar>.allocate(capacity: length + 1)
    buffer.initialize(from: cString, count: length + 1)
    
    // ... use `buffer` for authentication, etc. ...

    // Zero out the buffer after use
    memset(buffer, 0, length + 1)
    buffer.deallocate()
    // Also zero out the original cString from getpass
    memset(UnsafeMutableRawPointer(mutating: cString), 0, length)

    print("Press Enter to exit...")
    _ = readLine()
}Code language: JavaScript (javascript)

Po zastosowaniu tego podejścia dane wrażliwe znikają z pamięci:

Co ciekawe, nie istnieje żadne oficjalne API do takiego działania. Jeśli chcesz stworzyć wielokrotnego użytku „bezpieczny string”, możesz opakować bufor C w klasę Swift i zadbać o wyczyszczenie danych w deinit.

Model zagrożeń

Dla kontekstu odwołam się do macierzy MITRE ATT&CK, aby osadzić problem w szerszym ujęciu. To zagrożenie występuje wyłącznie wtedy, gdy malware uzyska dostęp do systemu. Odpowiada mu technika OS Credential Dumping.

Eksploitacja polega zazwyczaj na odczycie pamięci procesu za pomocą API systemowych lub poprzez debugger. Dodatkowo, w dokumencie CWE-316: Cleartext Storage of Sensitive Information in Memory możemy przeczytać, że dane wrażliwe mogą trafić na dysk w postaci zrzutów pamięci (core dump), albo „mogą zostać nieumyślnie ujawnione atakującym z powodu innej słabości” – dość otwarte zakończenie dla badaczy bezpieczeństwa.

W zależności od systemu operacyjnego, dostępne są pewne wbudowane mechanizmy ograniczające możliwość takiego odczytu.

Zabezpieczenia

MacOS oferuje domyślną ochronę przed wyciekiem wrażliwych danych z pamięci dzięki mechanizmowi Hardened Runtime, który uniemożliwia dołączanie debuggerów. Działa on jednak per-aplikacyjnie, a nie globalnie, poza tym deweloperzy nie zawsze go aktywują. Co więcej, ochrona ta nie jest w pełni odporna na obejścia ani błędne konfiguracje.

Linux ma domyślnie włączone ograniczenia ptrace. Nie powstrzymują one jednak użytkownika root przed dołączeniem do dowolnego procesu, w przeciwieństwie do Hardened Runtime. Co więcej, jeśli między procesami istnieje relacja rodzic-dziecko, mogą one wzajemnie odczytywać swoją pamięć. Root może też łatwo usunąć ograniczenie:

echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scopeCode language: PHP (php)

Windows… cóż, to po prostu Windows. Aplikacje działające pod tym samym użytkownikiem mogą uzyskiwać dostęp do swojej pamięci nawzajem.

Keeper Forcefield

Niedawno odkryłem to rozwiązanie i muszę przyznać, że robi wrażenie. Forcefield wypełnia realną lukę w modelu bezpieczeństwa Windows. Jest to sterownik jądra, który chroni procesy na niskim poziomie. Implementuje niestandardowe hooki w przestrzeni użytkownika, które przechwytują i blokują dostęp do określonych API, wzmacniając ochronę pamięci aplikacji.

Mimo że to solidne rozwiązanie, nadal pozostaje jedynie mitigacją, i działa tylko dla wybranych aplikacji.

Podsumowanie

Utrwalone przechowywanie danych wrażliwych w pamięci aplikacji to realne zagrożenie dla oprogramowania desktopowego na wszystkich głównych systemach operacyjnych. Choć istnieją mechanizmy ochrony na poziomie systemu operacyjnego, takie jak Hardened Runtime w macOS czy ptrace restrictions w Linuksie, ich skuteczność jest ograniczona. Ostatecznie to deweloperzy ponoszą odpowiedzialność za rozwiązanie tego problemu. Jak pokazuje przykład Swifta, języki wysokiego poziomu nie zawsze oferują odpowiednie API do bezpiecznego czyszczenia pamięci. Aby zminimalizować ryzyko, języki programowania powinny ewoluować w kierunku wbudowanych mechanizmów obsługi pamięci – np. funkcji gwarantującej wyzerowanie lub bezpiecznych struktur danych z automatycznym czyszczeniem.

Ponieważ brakuje spójnych standardów w tym zakresie, odpowiedzialność spada na programistów – a to niestety prowadzi do rozwiązań podatnych na błędy i trudnych do zweryfikowania.

Karol Mazurek
Head of Research

Czy Twoja firma jest bezpieczna w sieci?

Dołącz do grona naszych zadowolonych klientów i zabezpiecz swoją firmę przed cyberzagrożeniami już dziś!

Zostaw nam swoje dane kontaktowe, a nasz zespół skontaktuje się z Tobą, aby omówić szczegóły i dopasować ofertę do Twoich potrzeb. Dbamy o pełną dyskrecję i poufność Twoich danych, dlatego możesz nam zaufać.

Chciałbyś od razu zadać pytanie? Odwiedź naszą stronę kontaktową.