32.4.1 Versenyhelyzetek (race condition) azonosítása

2025.10.06.
AI Biztonság Blog

Képzelj el két automatizált ügynököt, akik egy közös számláról próbálnak API-krediteket levonni. Mindkettő egyszerre ellenőrzi az egyenleget: 100 kredit. Az első ügynök levonna 80-at, a második 50-et. Mivel egyszerre olvasták ki az állapotot, mindkettő úgy gondolja, van elég fedezet. Az első levonja a 80-at, az új egyenleg 20. A második, a régi 100-as információ alapján, levonja az 50-et. Az egyenleg -30 lesz. A rendszer integritása sérült. Ez a versenyhelyzet lényege.

Kapcsolati űrlap

AI Biztonság kérdésed van? Itt elérsz minket:

Mi is pontosan a versenyhelyzet?

A versenyhelyzet (race condition) egy olyan szoftverhiba, amely akkor lép fel, amikor egy rendszer vagy alkalmazás viselkedése a párhuzamosan futó, nem determinisztikus sorrendben végrehajtott műveletek időzítésétől függ. Más szavakkal, a végeredmény attól függ, hogy melyik folyamat vagy szál „nyeri meg a versenyt”, és ér el először egy megosztott erőforrást.

A probléma gyökere a nem atomi műveletekben rejlik. Egy látszólag egyszerű „ellenőrizd, majd írd felül” logika valójában több lépésből áll (olvasás, feldolgozás, írás). Ha egy másik folyamat beavatkozik ezen lépések között, az adatkonzisztencia elveszik.

Versenyhelyzetek az MI rendszerek kontextusában

Míg a klasszikus szoftverfejlesztésben a versenyhelyzetek jól ismertek, az MI-ökoszisztémák új és komplex felületeket nyitnak meg előttük:

  • Elosztott tréning: Több worker node párhuzamosan frissíti a központi paraméterszerver súlyait. Ha a frissítések (pl. egy gradiens alkalmazása) nem atomiak, inkonzisztens vagy sérült modellállapot jöhet létre.
  • Feature Store manipuláció: Egy folyamat frissít egy kritikus feature-t (pl. egy felhasználó kockázati besorolását), miközben egy másik folyamat éppen kiolvassa azt egy valós idejű inferencia számára. A modell a régi, már érvénytelen adatok alapján hoz döntést.
  • Online tanulási ciklusok: Egy rendszer folyamatosan új adatokkal finomhangolja magát. Egy támadó kihasználhatja az adatbetöltés és a modellfrissítés közötti időrést, hogy manipulatív adatot injektáljon, amely aránytalanul nagy hatással lesz a modellre, mielőtt a validációs mechanizmusok lefutnának.
  • Erőforrás-menedzsment: Több inferencia kérés versenyezhet egy limitált erőforrásért (pl. egy specifikus GPU-memória szegmens). Egy ügyesen időzített kéréssel esetleg olyan erőforráshoz lehet hozzáférni, ami egy másik, magasabb prioritású folyamat számára volt fenntartva.

A versenyhelyzet anatómiája

A szál B szál Idő Közös Erőforrás Olvasás (X=10) Írás (X=11) Olvasás (X=10) Írás (X=11) Probléma: Mindkét szál ugyanazt az értéket olvassa, felülírva egymás munkáját. A várt eredmény (X=12) helyett X=11 lesz.

Hogyan azonosítsd? Gyakorlati technikák

1. Kódanalízis

A legelső lépés a forráskód átvizsgálása. Keress olyan mintákat, ahol megosztott állapotot (globális változók, adatbázis-rekordok, fájlok, cache-elt értékek) több szál vagy folyamat írhat és olvashat szinkronizációs mechanizmus (lock, mutex, szemafor) nélkül. Különösen gyanúsak a „check-then-act” (ellenőrizd, majd cselekedj) minták, amelyek a következő, TOCTOU fejezet témáját képezik.

2. Terheléses tesztelés és Fuzzing

A versenyhelyzetek gyakran csak nagy terhelés alatt, vagy specifikus időzítési körülmények között jelentkeznek. Automatizált eszközökkel bombázd a rendszert nagy számú, párhuzamos kéréssel! Célzottan próbálj meg ugyanarra az erőforrásra irányuló kéréseket küldeni minimális időkülönbséggel. Figyeld a rendszer válaszait, a naplókat és az állapotváltozásokat anomáliák után kutatva (pl. negatív egyenleg, inkonzisztens adatok).

3. Időzítés-manipuláció

Haladóbb technika, amikor aktívan próbálod befolyásolni a végrehajtás sorrendjét. Ez megvalósítható a hálózati késleltetés mesterséges növelésével, vagy a rendszerre helyezett extra terheléssel (CPU vagy I/O), hogy az egyes szálak végrehajtási idejét megváltoztasd. A cél, hogy mesterségesen előidézd azt a ritka időzítési ablakot, ahol a sebezhetőség megnyilvánul.

Egy gyakorlati példa: API-kreditek manipulálása

Vegyünk egy egyszerű Python-alapú szolgáltatást, ami asyncio-t használ a kérések kezelésére. A felhasználóknak van egy kreditkeretük, amit az API-hívások csökkentenek.


# FIGYELEM: Sebezhető kód
import asyncio

# Megosztott erőforrás: felhasználói kreditek adatbázisa (szimulálva)
user_credits = {"user123": 100}

async def use_credits_vulnerable(user_id, amount):
 print(f"Aktuális egyenleg lekérdezése: {user_credits[user_id]}")
 # 1. Olvasás
 current_balance = user_credits[user_id]
 
 await asyncio.sleep(0.01) # Szimulálja a hálózati vagy feldolgozási időt
 
 if current_balance >= amount:
 # 2. Írás (a régi, már lehet, hogy érvénytelen adat alapján)
 user_credits[user_id] = current_balance - amount
 print(f"{amount} kredit levonva. Új egyenleg: {user_credits[user_id]}")
 return True
 else:
 print("Nincs elég kredit.")
 return False

async def main_vulnerable():
 # Két kérés indítása szinte egyszerre ugyanattól a felhasználótól
 await asyncio.gather(
 use_credits_vulnerable("user123", 80),
 use_credits_vulnerable("user123", 70)
 )
 print(f"\nVégső egyenleg (hibás): {user_credits['user123']}")
 # A várt eredmény az lenne, hogy az egyik tranzakció sikertelen.
 # Ehelyett az egyenleg negatív lesz.

A probléma megoldása egy lock bevezetése, ami biztosítja, hogy az olvasás-módosítás-írás ciklus atomi műveletként fusson le, megszakítás nélkül.


# Javított, biztonságos kód
import asyncio

user_credits_safe = {"user123": 100}
# A lock, ami megvédi a megosztott erőforrást
credit_lock = asyncio.Lock()

async def use_credits_safe(user_id, amount):
 async with credit_lock: # A lock megszerzése
 print(f"Lekérdezés (lock alatt): {user_credits_safe[user_id]}")
 current_balance = user_credits_safe[user_id]
 
 await asyncio.sleep(0.01) # A feldolgozási idő már nem veszélyes
 
 if current_balance >= amount:
 user_credits_safe[user_id] = current_balance - amount
 print(f"{amount} kredit levonva. Új egyenleg: {user_credits_safe[user_id]}")
 return True
 else:
 print("Nincs elég kredit.")
 return False
 # A lock automatikusan feloldásra kerül a 'with' blokk végén

async def main_safe():
 await asyncio.gather(
 use_credits_safe("user123", 80),
 use_credits_safe("user123", 70)
 )
 print(f"\nVégső egyenleg (helyes): {user_credits_safe['user123']}")

Konklúzió: Az időzítés, mint fegyver

A versenyhelyzetek azonosítása és kihasználása türelmet és a rendszer belső működésének mély megértését igényli. Nem egy egyszerű input-output sebezhetőségről van szó, hanem a rendszer dinamikus viselkedésének manipulálásáról. Red Teamerként feladatod, hogy ne csak azt vizsgáld, MIT csinál a rendszer, hanem azt is, hogy MIKOR és MILYEN SORRENDBEN teszi azt. A versenyhelyzet gyakran csak a jéghegy csúcsa, egy alacsony szintű hiba, amely utat nyithat komolyabb, üzleti logikát érintő támadásokhoz, mint amilyen a TOCTOU sebezhetőség is.