Unbeabsichtigter reentranter Aufruf von nicht-reentrantem Code über verschachtelte Aufrufe

Beschreibung

Unbeabsichtigter reentranter Aufruf von nicht-reentrantem Code über verschachtelte Aufrufe tritt auf, wenn ein Produkt Code aufruft, der als reentrant angenommen wird, aber verschachtelte Funktionsaufrufe unbeabsichtigt einen zweiten Aufruf von nicht-reentrantem Code auslosen und den Programmzustand unerwartet verändern. In komplexen Produkten kann ein einzelner Funktionsaufruf durch tief verschachtelte Aufrufe zu zahlreichen Codepfaden führen. Angreifer, die Eingaben manipulieren - insbesondere in Systemen, die nicht vertrauenswurdige Skripte ausführen wie Webbrowser - können unerwartete Kontrollflusse erreichen. Die Schwachstelle entsteht, wenn Codepfade den Programmzustand ändern, von dem der ursprungliche Aufrufer erwartet, dass er unverändert bleibt.

Risiko

Reentrancy-Schwachstellen haben schwerwiegende Sicherheitsauswirkungen. Use-after-free-Bedingungen können ausgelöst werden. Speicherkorruption kann auftreten. Unerwartete Zustandsänderungen können Absturze verursachen. Sicherheitsprüfungen können umgangen werden. Datenintegritat kann beeintrachtigt werden. Objektlebenszeiten können verletzt werden. Beliebige Codeausführung kann resultieren. Smart-Contract-Mittel können gestohlen werden.

Lösung

Fuhren Sie nicht vertrauenswurdige Event-Handler asynchron statt synchron aus und stellen Sie sicher, dass Aufrufe in nicht-reentranten Code strikt serialisiert werden. Achten Sie besonders auf Typkonvertierungspunkte. Stellen Sie sicher, dass Code reentrant ist, indem Sie nicht-lokale Datenmodifikationen vermeiden, Selbstmodifikation verhindern und Aufrufe an anderen nicht-reentranten Code vermeiden. Verwenden Sie Reentrancy-Guards und Mutex-Sperren für kritische Abschnitte.

Häufige Auswirkungen

AuswirkungDetails
IntegritatUmfang: Integritat

Unerwarteter Zustand - Ausnutzung kann die Anwendung in einem unerwarteten Zustand mit neu zugewiesenen Variablen hinterlassen.
IntegritatUmfang: Integritat

Speicherkorruption - Reentrancy kann Use-after-free und andere Speicherkorruptionsprobleme verursachen.
VertraulichkeitUmfang: Vertraulichkeit

Unbefugten Code ausführen - Speicherkorruption durch Reentrancy kann zu beliebiger Codeausführung führen.

Beispielcode und Lösung

Verwundbarer Code

// VERWUNDBAR: Widget-Klasse mit Reentrancy-Problem

class Image {
public:
    void click() {
        // Skript ausführen, das mit diesem Bild verknupft ist
        // VERWUNDBAR: Skriptausführung kann Reentrancy verursachen
        scriptEngine->executeScript(this->onClick_script);
    }

    ~Image() {
        // Destruktor
    }
};

class Widget {
private:
    Image* backgroundImage;
    int state;

public:
    Widget() {
        backgroundImage = new Image();
        state = 0;
    }

    void click() {
        state = 1;  // Zustand vor verschachteltem Aufruf setzen

        // VERWUNDBAR: Dies kann beliebiges Skript ausführen
        // Skript könnte changeBackgroundImage() aufrufen
        backgroundImage->click();

        // VERWUNDBAR: backgroundImage könnte geloscht worden sein!
        // Wenn Skript changeBackgroundImage() aufgerufen hat, haben wir jetzt
        // einen dangelnden Pointer
        state = 2;  // Diese Zeile nimmt an, dass backgroundImage noch gültig ist
    }

    void changeBackgroundImage(Image* newImage) {
        // VERWUNDBAR: Aufgerufen aus click() über Skript
        delete backgroundImage;  // Loscht das verwendete Objekt!
        backgroundImage = newImage;
    }
};

// Angriffsszenario:
// 1. Widget::click() wird aufgerufen
// 2. backgroundImage->click() führt Skript aus
// 3. Skript ruft widget->changeBackgroundImage(evilImage) auf
// 4. Originales backgroundImage wird geloscht
// 5. click() kehrt zu Widget::click() zurück
// 6. Widget::click() greift auf geloschtes backgroundImage zu -> UAF!
// VERWUNDBAR: Request-Klasse mit Reentrancy bei Typkonvertierung

class Request {
private:
    std::string uri;
    std::string credentials;
    bool sent;

public:
    void setup(const std::string& newUri, const std::string& newCreds) {
        uri = newUri;
        credentials = newCreds;
        sent = false;
    }

    void send() {
        if (sent) return;

        // VERWUNDBAR: String-Konvertierung kann Skript ausführen
        // toString() auf nicht vertrauenswurdigem Objekt
        std::string uriStr = scriptEngine->coerceToString(uri);

        // Skript könnte setup() mit anderen Anmeldedaten aufgerufen haben!
        // Jetzt haben wir einen inkonsistenten Zustand:
        // uriStr von alter uri, aber credentials von neuem setup()

        sendRequest(uriStr, credentials);
        sent = true;
    }
};

// Angriff:
// 1. request->setup("http://good.com", "goodCreds")
// 2. request->send() ruft coerceToString() auf
// 3. coerceToString führt Skript aus, das aufruft:
//    request->setup("http://evil.com", "evilCreds")
// 4. send() fahrt fort mit:
//    - uri-String von "http://good.com"
//    - credentials von "evilCreds"
// Ergebnis: Anmeldedaten an falschen Server gesendet!
// VERWUNDBAR: Smart-Contract-Reentrancy

contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // VERWUNDBAR: Externer Aufruf vor Zustandsaktualisierung
        // Die Fallback-Funktion des Angreifers kann withdraw() erneut aufrufen
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // VERWUNDBAR: Guthaben wird NACH externem Aufruf aktualisiert
        // Angreifer hat bereits erneut eingegriffen und abgehoben!
        balances[msg.sender] -= amount;
    }
}

// Angriffsvertrag:
contract Attacker {
    VulnerableBank public bank;

    constructor(address _bank) {
        bank = VulnerableBank(_bank);
    }

    function attack() public payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw(1 ether);
    }

    // Fallback-Funktion wird während withdraw aufgerufen
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            // ANGRIFF: Erneut in withdraw eintreten vor Guthabenaktualisierung
            bank.withdraw(1 ether);
        }
    }
}

Sichere Lösung

// SICHER: Widget-Klasse mit Reentrancy-Schutz

class Image {
public:
    void click() {
        // Sicher: Skript für verzögerte Ausführung planen
        scriptEngine->scheduleScript(this->onClick_script);
    }
};

class Widget {
private:
    Image* backgroundImage;
    int state;
    bool processingClick;  // SICHER: Reentrancy-Guard

    // SICHER: Referenzzahlung für sichere Objektlebensdauer verwenden
    std::shared_ptr<Image> backgroundImagePtr;

public:
    Widget() {
        backgroundImagePtr = std::make_shared<Image>();
        state = 0;
        processingClick = false;
    }

    void click() {
        // SICHER: Reentrancy-Guard
        if (processingClick) {
            return;  // Reentrante Aufrufe verhindern
        }
        processingClick = true;

        state = 1;

        // SICHER: Lokale Referenz beibehalten um Loschung zu verhindern
        std::shared_ptr<Image> localImage = backgroundImagePtr;

        // Selbst wenn Skript backgroundImagePtr ändert,
        // halt localImage das Objekt am Leben
        localImage->click();

        // SICHER: Objekt dank shared_ptr noch gültig
        state = 2;

        processingClick = false;
    }

    void changeBackgroundImage(std::shared_ptr<Image> newImage) {
        // SICHER: Altes Bild wird erst zerstört wenn alle Referenzen weg sind
        backgroundImagePtr = newImage;
    }
};

// Alternative: Asynchrone Event-Verarbeitung
class AsyncWidget {
private:
    Image* backgroundImage;
    std::queue<std::function<void()>> pendingEvents;
    bool processingEvents;

public:
    void click() {
        // SICHER: Event für asynchrone Verarbeitung einreihen
        pendingEvents.push([this]() {
            backgroundImage->click();
        });

        // Events nur auf oberster Ebene verarbeiten
        if (!processingEvents) {
            processEventQueue();
        }
    }

    void processEventQueue() {
        processingEvents = true;

        while (!pendingEvents.empty()) {
            auto event = pendingEvents.front();
            pendingEvents.pop();
            event();
        }

        processingEvents = false;
    }
};
// SICHER: Request-Klasse mit Reentrancy-Schutz

class Request {
private:
    std::string uri;
    std::string credentials;
    bool sent;
    bool inProgress;  // SICHER: Reentrancy-Guard
    std::mutex requestMutex;  // SICHER: Thread-sicherer Guard

public:
    void setup(const std::string& newUri, const std::string& newCreds) {
        std::lock_guard<std::mutex> lock(requestMutex);

        // SICHER: Setup während send verhindern
        if (inProgress) {
            throw std::runtime_error("Cannot modify request in progress");
        }

        uri = newUri;
        credentials = newCreds;
        sent = false;
    }

    void send() {
        std::lock_guard<std::mutex> lock(requestMutex);

        if (sent) return;

        // SICHER: Vor externen Aufrufen als in Bearbeitung markieren
        inProgress = true;

        // SICHER: Werte vor Konvertierung kopieren um TOCTOU zu verhindern
        std::string localUri = uri;
        std::string localCreds = credentials;

        // Jetzt kann Konvertierung unsere lokalen Kopien nicht beeinflussen
        std::string uriStr = scriptEngine->coerceToString(localUri);

        // SICHER: Lokale Kopien verwenden, immun gegen Reentrancy
        sendRequest(uriStr, localCreds);

        sent = true;
        inProgress = false;
    }
};

// Alternative: Zustand atomar erfassen
class SafeRequest {
private:
    struct RequestState {
        std::string uri;
        std::string credentials;
    };

    std::shared_ptr<RequestState> state;
    std::atomic<bool> sent;

public:
    void setup(const std::string& newUri, const std::string& newCreds) {
        // SICHER: Gesamten Zustand atomar ersetzen
        auto newState = std::make_shared<RequestState>();
        newState->uri = newUri;
        newState->credentials = newCreds;

        std::atomic_store(&state, newState);
        sent = false;
    }

    void send() {
        if (sent.exchange(true)) return;

        // SICHER: Unveränderlichen Snapshot des Zustands holen
        auto snapshot = std::atomic_load(&state);

        // Sicher: Snapshot ist unveränderlich, kann nicht durch Reentrancy beeinflusst werden
        std::string uriStr = scriptEngine->coerceToString(snapshot->uri);
        sendRequest(uriStr, snapshot->credentials);
    }
};
// SICHER: Smart Contract mit Reentrancy-Schutz

contract SecureBank {
    mapping(address => uint256) public balances;
    mapping(address => bool) private locked;  // SICHER: Reentrancy-Guard

    // SICHER: Modifier zur Verhinderung von Reentrancy
    modifier noReentrant() {
        require(!locked[msg.sender], "Reentrant call");
        locked[msg.sender] = true;
        _;
        locked[msg.sender] = false;
    }

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // SICHER: Checks-Effects-Interactions-Muster
    function withdraw(uint256 amount) public noReentrant {
        // Prüfungen
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // SICHER: Effekte VOR Interaktionen
        balances[msg.sender] -= amount;

        // Interaktionen (externer Aufruf) ZULETZT
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

// Alternative mit OpenZeppelins ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBankV2 is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // SICHER: nonReentrant-Modifier verwenden
    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Effekte vor Interaktionen
        balances[msg.sender] -= amount;

        // Sicherer externer Aufruf
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

CVE-Beispiele

  • Webbrowser-Schwachstellen, bei denen Skriptausführung während DOM-Operationen Use-after-free durch Reentrancy verursachte
  • Smart-Contract-Hacks einschließlich des DAO-Angriffs, der Reentrancy ausnutzte, um Millionen an Kryptowährung abzuziehen

Verwandte CWEs

  • CWE-662: Unsachgemäße Synchronisation (ubergeordnet)
  • CWE-663: Verwendung einer nicht-reentranten Funktion in einem parallelen Kontext (verwandt)
  • CWE-416: Use After Free (kann vorausgehen)
  • CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition (verwandt)

Referenzen

  1. MITRE Corporation. "CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls." https://cwe.mitre.org/data/definitions/1265.html
  2. The DAO Hack Analysis - Reentrancy Attack
  3. OpenZeppelin. "ReentrancyGuard"