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
| Auswirkung | Details |
|---|---|
| Integritat | Umfang: Integritat Unerwarteter Zustand - Ausnutzung kann die Anwendung in einem unerwarteten Zustand mit neu zugewiesenen Variablen hinterlassen. |
| Integritat | Umfang: Integritat Speicherkorruption - Reentrancy kann Use-after-free und andere Speicherkorruptionsprobleme verursachen. |
| Vertraulichkeit | Umfang: 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
- MITRE Corporation. "CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls." https://cwe.mitre.org/data/definitions/1265.html
- The DAO Hack Analysis - Reentrancy Attack
- OpenZeppelin. "ReentrancyGuard"