Web Uygulamalarında Race Condition Zafiyeti ve Etkileri

Merabalar

Race Condition, bir sistemin veya cihazın aynı kaynak üzerinden eş zamanlı olarak işlem yapmasıyla meydana gelir. Örneğin; Multithreading özelliği olan bir yazılımda, thread’ler birbirinden bağımsız bir şekilde ortak değişkene erişir ve değerini değiştirirse, devam eden süreçte her yerde aynı değere sahip olması olması gereken değişkenler, farklı thread’ler için farklı değerler ile işlemlerine devam edebilir. Buda istenmeyen sonuçlara neden olacaktır.

İşlemcinin Process Anahtarlaması

Modern işletim sistemleri, multitasking adı verilen, eş zamanlı olarak birden fazla işlemi yürütülebilme özelliğine sahiptirler. Bu sürece CPU’nun komut çalıştırma döngüsü bazında baktığımızda, aslında multitasking’in gerçekten var olmadığı fark edilebilir.

İşlemciler, birden fazla süreci eş zamanlı olarak işletebilmek yada en azından son kullanıcılar açısından böyle hissedilebilmesi adına, çalışan süreçler arasında sürekli geçiş yapmaktadırlar. Daha sade bir ifade ile, bir sürecin işi bitmeden kendisini durdurup başka bir süreç işleme alınmaktadır. Bu sürecin yönetimide işletim sistemi tarafından gerçekleştirilmektedir. Örneğin en ilkel anahtarlama algoritmalarından birisi olan Round Robin algoritması aşağıdaki şekilde çalışır.

round robin

Her process’in yani sürecin CPU’da işlem görme hakkı işletim sistemi tarafından belirlenen bir zaman dilimi kadardır. Örneğin; 2 microsaniye. CPU sırasıyla her process’i alır 2 microsaniye boyunca çalışacan komutlarını çalıştırır, sürecin bitip bitmediğine bakmaksızın bir sonraki sürec’e geçer. Böylece son kullanıcılar açısından birden fazla süreç eş zamanlı olarak çalışıyor gibi gözüksede, aslında CPU bazından işler hala sırasıyla yapılmaktadır.

Fark ettiğiniz üzere Round Robin algoritması herhangi bir optimizasyon ve işlem önceliği gibi kavramlara sahip olmadığı için son derece ilkel bir yaklaşımdır. Günümüzde kullanımı adeta yoktur.

Tüm bu bilgiler ışığında, 2 adet thread’in süreli 2 microsaniye boyunca işlem gördüğünü düşünelim. Bu durumda eğer thread’ler, erişim sağladıkları ortak değişkenlere erişimlerinde birbirlerini kontrol etmezlerse, race condition zafiyeti doğabilir.

Basit bir Web Uygulaması ve Race Condition Örneği

Aşağıda, Flask ile yazılmış basit bir para işlemi gerçekleştiren web uygulamasının kaynak kodları bulunmaktadır.

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)

class Hesap(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    miktar = db.Column(db.String(80), unique=True)

    def __init__(self, miktar):
        self.miktar = miktar 

    def __repr__(self):
        return '<Miktar %r>' % self.miktar

@app.route("/")
def hello():
    hesap = Hesap.query.get(1) # Sadece bir adet cuzdan var.
    return "Toplam para = {}".format(hesap.miktar)

@app.route("/gonder/<int:miktar>")
def gonder(miktar):
    hesap = Hesap.query.get(1) 
    if int(hesap.miktar) < miktar:
        return "Yetersiz bakiye. /reset ile paralari sifirla:)"
    hesap.miktar = hesap.miktar - miktar
    db.session.commit()
    return "Gonderilen miktar = {}".format(miktar)

@app.route("/reset")
def reset():
    hesap = Hesap.query.get(1)
    hesap.miktar = 10000
    db.session.commit()
    return "Paralari sifirladik."

if __name__ == "__main__":
    app.secret_key = 's0fuck1ngs3cret!'
    app.run()

Bu kodu çalıştırmadan önce manuel olarak hesap tablosuna bir adet kayıt oluşturup üzerinden işlemlere devam edeceğiz. Kod üzerinde gördüğünüz üzere, test ortamı olduğu için tabloda ki ilk kayıt üzerinden işlem yapılmaktadır.

>>> from para import db
>>> db.create_all()
>>> from para import Hesap
>>> hesap = Hesap(10000)
>>> db.session.add(hesap)
>>> db.session.commit()

Böylece 10.000 TL bakiyesi bulunan bir hesap oluşturmuş durumdayız. Son olarak yukarıdaki kaynak kodu Flask ve Flask-SQLAlchemy paketlerinin kurulu olması şartı ile python para.py komutu ile çalıştırıyoruz. Böylece basit bir çıkartma işlemi yapan web uygulamasına sahip olmuş durumdayız. Bu uygulama aşağıdaki GET talebi linkleri ile şu işlemleri yapabilmektedir.

http://127.0.0.1:5000/ = Mevcut bakiyeyi görüntülüler

http://127.0.0.1:5000/gonder/MIKTAR = Belirtilen MIKTAR çıkartılır ve veri tabanına kayıt edilir.

http://127.0.0.1:5000/reset = Paraları, kutular ile sıfır. 10.000 TL atama yapar.

Race Condition İhtimali

Yukarıdaki web uygulamasını ele aldığımızda, muhtemel bir race condition zafiyeti olduğu görülmektedir. Peki nasıl bir senaryoda bu Race Condition oluşur ?

Başlangıçta 10.000 TL paramızın olduğunu ve 1 TL para transferi yapacak iki farklı HTTP talebi oluşturalım. http://127.0.0.1:5000/gonder/1 linkine iki farklı HTTP talebi gönderilerek bu işlem yapılabilir. İlk talebi alıp işleme koyan web sunucusunun process’i aşağıdaki satırı çalıştırdığı andan hemen sonra CPU’nun bu process’i durdurup gelen 2. talebi işleme aldığını kabul edelim.

hesap.miktar = int(hesap.miktar) - miktar

2. talebi işleme alıp, tüm süreci tamamladıktan sonra tablo şu şekilde olacaktır. NOT: 1. process’de çıkartma işlemi yapıldı ama henüz veri tabanına kayıt edilmedi.

Veri tabanında miktar değeri 9999 halindedir. 2. process tamamlanmıştır. 1. process ise tam veri tabanına kayıt gerçekleştireceği anda çalışması durdurulmuştu. CPU 1. process’e dönüp işlemini tamamlamak istediğindeyse veri tabanına 1. process’in çıkartma işleminin sonucu olan 9999 tekrar atanacakır!

Sonuçta 10.000 – 2 = 9998 TL para olması gereken hesaptan 2 TL EFT yapılmıştır ama hesabın bakiyesi 9999 TL olacak kalmıştır.

Saldırı

Hedef sisteme, 5 saniye gibi bir zaman dilimi içerisinde tam olarak 128 adet 1 TL transfer yapma talebi gönderilmiştir. Bu işlem sonucunda beklenen hesap özeti aşağıdaki gibidir.

Başlangıç para miktarı = 10000

Transfer edilen para miktarı = 128×1 = 128

Beklenen kalan miktar = 10000 – 128 = 9872

Veri tabanındaki kalan miktar = 9875

Arada  ki 3 TL fark, önceki adımlarda anlatılan, process’lerin CPU’a anahtarlanması sırasındaki karmaşıklık nedeniyle gerçekleşmektedir. Tabi buradaki en önemli etken ise, programcının yazdığı kod üzerinde herhangi bir “kritik bölge” kararının verilmemesi nedeniyle semaphore benzeri yapıların kullanılmamasıdır.