Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr to en Georges Tarbouriech
en to tr Erdal MUTLU
Christophe Blaess bağımsız bir aeronotic mühendisidir. Kendisi Linux hayranıdır ve ilşrerinin birçoğunu Linux altında yapmaktadır. Linux Kaynakyazılandırım Projesi (Linux Documentatiın Project) tarafından yayımlanan man sayfalarının çevirilmesini yönetmektedir.
Christophe Grenier ESIEA'da öğrenci olarak 5. yılındadır ve aynı zamanda burada sistem yöneticiliği yapmaktadır. Bilgisayar güvenliği konusunda özel merakı vardır.
Frédéric Raynal, çevreyi kirletmediği, hormon kullanılmadığı, MSG veya hayvansal malzemeler kullanılmadığı ve sadece tatlıdan oluştuğu için Linux işletim sistemini yıllardır kullanmaktadır.
Az rastlanır kuşullar için genel ilke şudur : bir süreç sistem kaynağını sadece kendisi ulaşmak istiyor. Süreç, başka bir sürecin bu kaynağı kullanıp kullanmadığını denetlemekte ve ondan sonra bu kaynağın kullanımını ele geçirmekte ve istediği gibi kullanmaktadır. Problem, ilk sürecin kaynağının serbest olup olmadığının denetimi ile gerçek kullanıma geçme zamanı arasında, başka bir sürecin bu kaynağı kullanmak istediği durumda ortaya çıkmaktadır. Sunuç duruma göre değişiklik göstermektedir. İşletim sistemleri teorisindeki klasik duruma göre, her iki süreç sonsuz döngüye girer ve kilitlenirler. Pratikte ise, bu durum uygulamalarının yanlış çalışmasına ve bir sürecin yanlışıkla diğerinin haklarından faydalanması durumunda ise, gerçek güvenlik açıklarına neden olmaktadır.
Daha önce kaynak olarak adlandırdığımız seyin farklı anlamları vardır.
Az rastlanır koşulların birçoğu, çekirdeğin bellek alanlarının kullanımıyla ilgili
ortaya çıkmıştır ve düzeltilmiştir. Biz burada sistem uygulamalarına odaklanacağız ve
ilgileneceğimiz kaynaklar da dosya sistemindeki inode'lar olacaktır. Bu sadece sıradan
dosyaları değil, aynı zamanda /dev/
dizininde bulunan dosyalar aracılığı ile
sistem aygıtlarının kullanımını da kapsamaktadır.
Genelde sistem güvenliğine yönelik yapılan saldırılar, Set-UID
olan uygulamalara yapılmaktadır. Saldırıyı yapan kişi, programın sahip
olduğu kullanıcı haklarından yararlandığı sürece programı kullanacaktır.
Daha önce anlattığımız güvenlik açıklarına (bellek taşması, biçimlendirme
katarlarları, ...) karşın, az rastlanır koşulların ortaya çıkarttığı güvenlik açıkları,
"özel" program çalıştırılmasına genellikle izin vermemektedir.
Sadece program çalıştığı sürece,
programın sahip olduğu kaynaklardan yararlanmaya olanak sağlamaktadır.
Bu şekilde yapılan saldırılar sadece Set-UID olan uygulamalara değil,
kullanıcılarının çalıştırdığı "normal" uygulamalara yönelik de yapılmaktadır.
Saldırgan, genellikle pusuda yatmakta ve başka bir kullanıcının, özellikle
"root" kullanıcısının ilgili programı çalıştırmasını beklemektedir ki bu
programın sahip olduğu kaynakları kullanabilsin. Kaynak kullanımı bir dosyaya yazma
(Sözgelimi, ~/.rhost
dosyasına "+ +
" yazarak, herhangi bir
bilgisayardan bu bilgisayara
şifresiz ve root haklarına sahip olarak ulaşabilmektedir.)
veya gizli yada önemli bilgiler içeren dosyalardan okuma (önemli ticari bilgileri,
kişisel sağlık bilgileri, şifre dosyası bilgileri, özel anahtar bilgileri vs)
haklarına sahip olmaktadır.
Önceki makalelerde konuşulan güvenlik açıklarından farklı olarak, bu güvenlik sorunu Set-UID olan uygulamalarını, sistem sunucularınını, daemonlarını, kısaca hertürlü uygulamayı tehtid etmektedir.
İlk olarak, Set-UID olan ve kullanıcının dosyasına bilgi yazacak bir programın davranışlarını inceleyelim. Biz, sözgelimi sendmail gibi, elektronik ileti gönderici yazılım programını da ele alabilirdik. Diyelim ki, kullanıcı bir dosya adı ile bu dosyaya yazılacak bilgiyi programa parametre olarak verebilsin. Buna göre program, kendisini çalıştıran kişinin yazılacak dosyanın sahibi olup olmadığını denetleyecektir. Ayrıca, dosyanın sistem dosyalarından birine bağlantı dosyası olup olmadığını da denetlemesi gerekecektir. Unutmamak gerekir ki, Set-UID olan bir program, istediği dosyayı değiştirme hakkına sahiptir. Buna göre, kendi gerçek UID ile dosya sahibinin kullanıcı numarasını karşılaştırmak gerekecektir. Şimdi şöyle bir program yazalım :
1 /* ex_01.c */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <sys/stat.h> 6 #include <sys/types.h> 7 8 int 9 main (int argc, char * argv []) 10 { 11 struct stat st; 12 FILE * fp; 13 14 if (argc != 3) { 15 fprintf (stderr, "usage : %s file message\n", argv [0]); 16 exit(EXIT_FAILURE); 17 } 18 if (stat (argv [1], & st) < 0) { 19 fprintf (stderr, "can't find %s\n", argv [1]); 20 exit(EXIT_FAILURE); 21 } 22 if (st . st_uid != getuid ()) { 23 fprintf (stderr, "not the owner of %s \n", argv [1]); 24 exit(EXIT_FAILURE); 25 } 26 if (! S_ISREG (st . st_mode)) { 27 fprintf (stderr, "%s is not a normal file\n", argv[1]); 28 exit(EXIT_FAILURE); 29 } 30 31 if ((fp = fopen (argv [1], "w")) == NULL) { 32 fprintf (stderr, "Can't open\n"); 33 exit(EXIT_FAILURE); 34 } 35 fprintf (fp, "%s\n", argv [2]); 36 fclose (fp); 37 fprintf (stderr, "Write Ok\n"); 38 exit(EXIT_SUCCESS); 39 }
Daha önceki makalede de anlattığımız gibi, Set-UID olan uygulamanın geçici olarak sahip olduğu hakları bir yana bırakıp, kendisini çağıran kullanıcının UID'si ile dosyayı açmayı denemesi daha iyi olacaktır. Bu arada, yukarıdaki program, bir daemon programın, kullanıcılara verdiği hizmeti temsil etmektedir. Her zaman root olarak çalışarak kendi gerçek UID'si yerine kullanıcı UID'sini denetleyebilir. Her neyse, gerçekçi olmasa da sorunu anlamada ve güvenlik açığından "faydalanma" bakımından kolaylık sağladığı için bu şemaya sadık kalacağız.
Gördüğünüz gibi program çalıştığında, gerekli tüm denetimleri yerine getirmektedir, yani
dosyanın var olup olmadığını, kullanıcıya ait olup olmadığını ve sıradan bir dosya
olup olmadığını denetlemektedir. Daha sonra dosya açılmakta ve verilen bilgi içine
yazılmaktadır. İşte, güvenlik açığı da burada ortaya çıkmaktadır! Daha açık olmak gerekirse,
güvenlik açığı, stat()
fonksiyonu ile dosya özelliklerini belirlemekle,
fopen()
fonksiyonu ile de dosyayı gerçekten açma arasında
geçen zaman dilimindedir. Bu zaman aralığı oldukça kısadır, ancak saldırgan, bu
açıktan faydalanabilir ve dosyanın özelliklerini bu arada değiştirebilir. Saldırımızı
kolaylaştırmak ve bu arda gerekli değişikliği elle yapabilmek için, iki işlem
arasında geçen zamanı artıralım. Daha önce boş olan 30. satıra sürecin uykuya
yatacağı (bekleme) komutunu, aşağıdaki gibi ekleyelim:
30 sleep (20);
Şimdi, bir uygulama yapalım. İlk önce uygulamayı (programı), Set-UID root olarak
ayarlayalım. Ayrıca, şifrelerinin bulunduğu /etc/shadow
dosyasının bir
yedeğini alalım. Bu çok önemlidir.
$ cc ex_01.c -Wall -o ex_01 $ su Password: # cp /etc/shadow /etc/shadow.bak # chown root.root ex_01 # chmod +s ex_01 # exit $ ls -l ex_01 -rwsrwsr-x 1 root root 15454 Jan 30 14:14 ex_01 $
Saldırgan için herşey hazır. Kendi kullanıcımıza ait bir dizindeyiz.
Güvenlik açığı ve Set-UID root olan bir uygulamamız (ex_01
) var.
Yapmak istediğimiz şey ise, /etc/shadow
şifre dosyasındaki
root kullanıcısına ait şifreyi, boş bir şifreyle değiştirmektir.
İlk önce fic
adında kendimize ait bir dosya yaratıyoruz:
$ rm -f fic $ touch fic
Daha sonra uygulamamızı arka planda çalıştırıyoruz. Programdan, ona verdiğimiz dosyaya gerekli bilgiyi yazmasını istiyoruz. Program, gerekli denetimleri yaptıktan sonra, dosyaya yazmadan once biraz beklemektedir.
$ ./ex_01 fic "root::1:99999:::::" & [1] 4426
root
satırının içeriği, shadow(5)
man sayfasına
göre hazırlanmıştır ve en önemli kısmı, şifre yok anlamına gelen ikinci alanın
boş olmasıdır. Program beklemekte iken, fic
dosyasını silip,
/etc/shadow
dosyasına bir simgesel (symbolic) veya gerçek (physical) bağlantı
yapmak (ikiside oluyor) için yaklaşık 20 saniyemiz vardır.
Bir hatırlatma yapalım, isteyen herkes bir dosyaya, kendisinin dosyada okuma hakkı
olmasa bile, kendi veya /tmp
dizininde bağlantı yaratabilir
(bunu birazdan göreceğiz). Dosyanın bir kopyasını, ancak okuma hakkı
olduğu zaman oluşturulabilmektedir.
$ rm -f fic $ ln -s /etc/shadow ./fic
Daha sonra kabuktan, ex_01
sürecini ön plana almasını istiyoruz ve programın
sonlanmasını bekliyoruz:
$ fg ./ex_01 fic "root::1:99999:::::" Write Ok $
İşte, herşey bitti! /etc/shadow
dosyasında artık, root kullanıcısına ait
şifre alanı boş, yani şifre yok. İnanmıyor musunuz?
$ su # whoami root # cat /etc/shadow root::1:99999::::: #
Eski şifre dosyasını yerine koyarak deneyimimizi tamamlayalım:
# cp /etc/shadow.bak /etc/shadow cp: replace `/etc/shadow'? y #
Set-UID root olan bir uygulamada az rastlanır koşuldan dolayı ortaya çıkan güvenlik açığından faydalanmayı başardık. Tabii, dosyayı değiştirmek için 20 saniye zaman tanımakla programın, bize oldukça "yardımı" dokundu. Gerçek uygulamalarda ise, bu tür zaman aralıkları çok kısa olmaktadır. Peki bunun için ne yapılabilir?
Genell prensip, denemeleri otomatik yapan kabuk programları kullanmak ve deneme sayısını yüzlerce, binlerce hatta onbinlerce kez tekrarlamak, yani aktif saldırı yöntemi kullanılmaktadır. Güvenlik açığından faydalanabilme şansını artırmak, bizim durumuzda iki işlem arasındaki zamanı artırmak için çeşitli hilelerden faydalanılabilir. Buradaki amaç,dosya üzerindeki değiştirme işlemini yerine getirmeden önce, programı mümkün olduğu kadar yavaşlatmaktır. İstediğimizi elde etmek için çeşitli yaklaşımlar kullanabiliriz:
nice -n 20
getirerek, mümkün olduğu kadar yavaşlatmak.
while (1);
) kullanarak, harcayarak sistem yükünü artırmak.
Az rastlanır koşulların ortaya çıkarttığı güvenlik açıklarından faydalanma yöntemi tekrar gerektirmektedir ve oldukça sıkıcı bir işlemdir. Aynı zamanda da oldukça kullanışlıdır! Şimdi bizi çözüme götürecek en etkili yöntemleri bulmaya çalışalım.
Yukarıda anlattığımız sorun, bir nesne üzerinde iki işlem arasında
geçen zaman içerisinde, bu nesnenin özelliklerini değiştirebilmekte yatmaktadır.
Sıradan kullanıcı olarak /etc/shadow
dosyasını değiştirmek ve hatta
okumak oldukça zor bir işlemdir. Gerçekte, değişiklik dosyanın var olan ağaç
yapısındaki bağlantısı ile fiziksel olarak dosyanın kendisi arasındadır.
rm
, mv
, ln
gibi sistem komutlarının
birçoğunun dosya içeriği üzerinde değil, dosya adı üzerinde işlem yapmaktadırlar.
rm
ve unlink()
sistem çağrısı ile dosya silme işleminde bile,
dosyanın içeriği, ancak, dosyaya son fiziksel bağlantısı kesildikten sonra silinmektedir.
Dolayısıyla, daha önceki programda yaptığımız hata, stat()
ve
fopen()
işlemleri arasında geçen zaman diliminde, dosya adı ile içeriği
arasındaki ilişkinin sabit kalacağını düşünmemizdir. Bu ilişkinin hiç te
sabit kalmadığını anlamak için, dosyalardaki fiziksel bağlantı örneğini ele alabiliriz.
Kendimize ait olan bir dizinde, bir sistem dosyasına bağlantı oluşturalım.
Dosya ile ilgili olan dosyanın sahibi ve erişim hakları değişmeden kalacaktır tabii.
ln
komutundaki -f
seçeneği sayesinde, bağlantı için verilecek aynı
isimdeki bir dosya var olsa bile, bağlantı yinede oluşturulmaktadır:
$ ln -f /etc/fstab ./myfile $ ls -il /etc/fstab myfile 8570 -rw-r--r-- 2 root root 716 Jan 25 19:07 /etc/fstab 8570 -rw-r--r-- 2 root root 716 Jan 25 19:07 myfile $ cat myfile /dev/hda5 / ext2 defaults,mand 1 1 /dev/hda6 swap swap defaults 0 0 /dev/fd0 /mnt/floppy vfat noauto,user 0 0 /dev/hdc /mnt/cdrom iso9660 noauto,ro,user 0 0 /dev/hda1 /mnt/dos vfat noauto,user 0 0 /dev/hda7 /mnt/audio vfat noauto,user 0 0 /dev/hda8 /home/ccb/annexe ext2 noauto,user 0 0 none /dev/pts devpts gid=5,mode=620 0 0 none /proc proc defaults 0 0 $ ln -f /etc/host.conf ./myfile $ ls -il /etc/host.conf myfile 8198 -rw-r--r-- 2 root root 26 Mar 11 2000 /etc/host.conf 8198 -rw-r--r-- 2 root root 26 Mar 11 2000 myfile $ cat myfile order hosts,bind multi on $
/bin/ls
komutundaki -i
seçeneği sayesinde, satır başında
dosyanın sahip olduğu inode numarası görüntülenmektedir. Dolayısıyla, aynı dosya
isminin iki farklı inode numarasını işaret ettiğini görmekteyiz.
Gerçek şu ki biz, dosyanın özelliklerini denetleyen ve içeriğine ulaşan fonksiyonların,
aynı içeriğe ve inode numarasına sahip dosyaya ulaşsınlar isteriz.
Ve bu mümkündür! Çekirdeğin kendisi bize dosya erişim numarası (file descriptor)
verdiğnde, dosya adı ve içeriği arasındaki ilişkinin sabit kalmasını
otomatik olarak sağlamaktadır. Okumak için bir dosyayı open()
sistem çağrısı (system call) ile açtığımızda, bir tamsayı geri göndermektedir.
Dosya erişim numarası olan bu tamsayı, dosya adı ile içeriği
arasındaki ilişkiyi, sistemin tuttuğu iç tablolarda sağlamaktadır.
Daha sonra yapılacak olan tüm okuma işlemleri, dosya adına ne olursa olsun,
dosya açma işlemi sırasındaki içerik neyse, onun üzerinden yapılacaktır.
Bunu biraz daha vurgulayalım: dosya bir kere açıldıktan sonra, dosya adı ile
ilgili yapılacak tüm işlemlerin, buna silmek te dahil, dosya içeriği üzerinde
hiç bir etkisi olmayacaktır. Dosya erişim numarasına sahip olan süreçler olduğu
sürece dosyanın, bulunduğu dizinden adı silinse bile, içeriği disk üzerinden
silinmeyecektir. open()
ile close()
sistem çağrıları
arasında geçen zaman aralığında, dosya içeriği ile olan ilişkinin
sabit kalacağını çekirdek garanti etmektedir.
O zaman çözümümüzü bulduk demektir! İlk önce işe dosyayı açmakla başlamalı ve
dosya özelliklerini (haklarını), dosya erişim numarası aracılıyla
daha sonra denetlemelidir. Bunun için stat()
gibi çalışan,
ancak dosya adı yerine, dosya erişim numarası parametre olarak alan
fstat()
sistem çağrısını kullanabiliriz. Daha sonra dosya üzerinde
GÇ (IO=Input Output, GÇ=Giriş Çıkış) yapmak için fopen()
gibi
çalışan, ancak dosya adı yerine, dosya erişim numarası parametre olarak alan
fdopen()
fonksiyonunu kullanacağız. Dolayısıyla program, aşağıdaki gibi
olmaktadır:
1 /* ex_02.c */ 2 #include <fcntl.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <unistd.h> 6 #include <sys/stat.h> 7 #include <sys/types.h> 8 9 int 10 main (int argc, char * argv []) 11 { 12 struct stat st; 13 int fd; 14 FILE * fp; 15 16 if (argc != 3) { 17 fprintf (stderr, "usage : %s file message\n", argv [0]); 18 exit(EXIT_FAILURE); 19 } 20 if ((fd = open (argv [1], O_WRONLY, 0)) < 0) { 21 fprintf (stderr, "Can't open %s\n", argv [1]); 22 exit(EXIT_FAILURE); 23 } 24 fstat (fd, & st); 25 if (st . st_uid != getuid ()) { 26 fprintf (stderr, "%s not owner !\n", argv [1]); 27 exit(EXIT_FAILURE); 28 } 29 if (! S_ISREG (st . st_mode)) { 30 fprintf (stderr, "%s not a normal file\n", argv[1]); 31 exit(EXIT_FAILURE); 32 } 33 if ((fp = fdopen (fd, "w")) == NULL) { 34 fprintf (stderr, "Can't open\n"); 35 exit(EXIT_FAILURE); 36 } 37 fprintf (fp, "%s", argv [2]); 38 fclose (fp); 39 fprintf (stderr, "Write Ok\n"); 40 exit(EXIT_SUCCESS); 41 }
Bu sefer 20. satırdan sonraki programımızın çalışmasını, dosya adı üzerinde yapılan hiçbir değişiklik (silme, değiştirme, bağlantı kurma) etkilemeyecektir ve orijinal fiziksel dosyanın içeriği korumnuş olacaktır.
Demekki, dosya üzerinde işlem yaparken, dosyanın gerçek içeriği ile gösterilimi arasındaki ilişkinin sabit kaldığını sağlamak gerekir. Dolayısıyla, tercihimiz, dosya adı üzerine değil de, daha önceden açılmış ve dosya erişim numarası üzerinde işlem yapan, aşağıdaki sistem çağrılarını kullanmak olmalıdır:
Sistem çağrısı | Kullanımı |
fchdir (int fd) |
fd ile gösterilen dizine gider. |
fchmod (int fd, mode_t mode) |
Dosya erişim haklarını değiştir. |
fchown (int fd, uid_t uid, gid_t gif) |
Dosya sahibini değiştirir. |
fstat (int fd, struct stat * st) |
Fiziksel dosyanın erişim numarası ile birlikte tutulan bilgilere ulaşır. |
ftruncate (int fd, off_t length) |
Var olan dosyanın içeriğini siler. |
fdopen (int fd, char * mode) |
Dosya üzerinde GÇ (IO) yapılmasını sağlamaktadır. Bu bir sistem çağrısı değil, stdio kütüphanesinde tanımlanmış bir fonksiyondur. |
İşe dosyayı open()
sistem çağrısı ile istenilen erişim düzeninde
açmakla (yeni dosya yaratırken üçüncü parametreyi unutmayın) başlamalı. Geçici
dosya yaratırken open()
sistem çağrısı üzerinde ayrıntılı olarak
duracağız.
Sistem çağrılarını kullanırken, dönüş deşerleri mutlaka denetlenmelidir.
Az rastlanır koşullar ile ilgili olmasa da, dönüş değerlerini
denetlememenin önemini, /bin/login
programındaki eski bir hatadan söz ederek
gösterelim.
Bu uygulama, /etc/passwd
dosyasını bulamadığı zaman, otomatik
olarak sisteme root olarak girilmesini sağlamaktaydı. Eğer, zarar görmüş
bir dosya sistemini tamir etmek amacıyla olursa bu durum kabul edilebilirdi.
Diğer taraftan, dosyanın gerçekte var olup olmadığını denetlemeden, sadece dosya
açılamıyor diye, böyle bir davranışta bulunulması pek kabul edilebir
değildir. En büyük dosya erişim sayısına eriştikten sonra,
/bin/login
programı çalıştırlması sisteme root olarak girmeye
yeterli olmaktadır. Sistem güvenliği için dönuş ve hata değerlerini
denetlemenin öneminde söz etmeyi bir yana bırakalım ve konumuza devam edelim..
Sistem güvenliğini ilgilendiren bir program, bir dosyaya erişirken sadece kendisinin eriştiğini kabul ederek hareket etmemelidir. Aynı dosya üzerinde oluşabilecek ar rastlanır koşulları ortadan kaldırmak gerekmektedir. En önemli tehlike, Set-UID root olan bir programı kullanıcı aynı anda bir kaç adet çalıştırarak veya aynı daemon programıyla birden fazla bağlantı kurmaya çalışarak, az rastlanır koşul yaratıp, bu arada dosyanın içeriğini daha önceden öngörülmemiş bir şekilde değiştirmesinde ortaya çıkmaktadır.
Bu durumun önüne geçebilmek için, bir dosya üzerine aynı anda sadece bir programın ulaşabileceği bir yöntem geliştirmelidir. Bu veritabanı sistemlerinde, birden fazla kullanıcının aynı dosya üzerinden sorgu yapması veya değişiklik yapmak istemesi sorunu ile aynıdır. Sorunumuzun çözümü, dosya kilitleme yöntemidir.
Dosya üzerinde veya dosyanın bir kısmında bir yazma işlemei olacağı zaman, süreç, çekirdek tarafından burayı kilitlemesini istemektedir. Kilit sürecin elinde bulunduğu sürece, başka hiç bir süreç aynı dosya veya dosyanın aynı kısmı için kilit isteyemiyor. Dolayısıyla, dosya üzerine bir yazma işlemi yapmadan önce, süreç bu dosya için bir kilit elde etmekte ve yazma işlemini gerçekleştirmektedir. Bu arada dosyayı başkası değiştirememektedir.
Gerçekte sistem daha akıllı davranmaktadır. Çekirdek, dosya yazma ile dosyadan bilgi okuma amaçlı verilen kilitleri ayrı tutmaktadır. Dolayısıyla, dosya içeriği değiştirilmeyeceği için, birden fazla süreç, dosya okuma kilidi elde edebilir. Ancak, aynı anda sadece bir süreç yazma kiliti alabilir ve bu arada başka süreçlere okuma kilidi dahi verilmez.
Birbiri ile uyumlu olmayan iki tip kilit vardır. İlki BSD sistemlerinden gelmekte ve
flock()
sistem çağrısı ile elde edilmektedir. Sistem çağrısına verilen
ilk parametre, kilitlemek istenilen dosyanın, dosya erişim numarasıdır. İkinci
parametre ise, yapılacak işlemi nitelendiren bir sabittir. Sabitin alacağı değerler
şunlardır : LOCK_SH
(okumak için kilitle), LOCK_EX
(yazmak için kilitle), LOCK_UN
(kiliti serbest bırak). İstenilen
kilitleme şekli elde edilene kadar program beklemektedir. Ancak, istenilirse,
LOCK_NB
seçeneği, ikili VEYA |
ile birleştirilebilir.
Bu durumda, kilit elde edilemediğinde, sistem çağrısı geri dönmektedir ve böylece dönüş
değeri denetlenerek, program beklemekte kurtulabilir.
İkincisi kilitleme şekli, System V sistemlerinden gelmektedir ve fcntl()
sistem çağrısı ile olmaktadır. Ancak kullanımı biraz karışıktır.
Aynı şekilde çalışmasa da, lockf()
adında bir kütüphane fonksiyonu
da vardır. fcntl()
sistem çağrısının ilk parametresi,
işlem yapılacak dosyanın erişim numarasıdır. İkinci parametre
yapılacak işlemi nitelendirmektedir : F_SETLK
kilit elde edilemezse
hemen geri dönmektedir ve F_SETLKW
kilit elde edilene kadar
beklemektedir. F_GETLK
seçeneği ile kilit hakkında bilgi edinmek mümkündür
(Bu bizim durmda bir anlam ifade etmemektedir.).
Üçüncü parametre ise, kilit hakkında bilgi edinebileceğimiz
struct flock
tipinde bir veri yapısına
işaret eden bir işaretçidir. flock
veri yapısının elemanları şunlardır:
Ad | Tip | Anlamı |
l_type |
int |
Beklenen çalışma şekli :
F_RDLCK (okumak için kilitleme),
F_WRLCK (yazmak için kilitleme) ve
F_UNLCK (kiliti aömak için). |
l_whence |
int |
l_start alanı başlangıcı (genelde
SEEK_SET ). |
l_start |
off_t |
Kilitin başlangıç yeri (genelde 0). |
l_len |
off_t |
Kilitin uzunluğu, dosya sonuna kadar ise 0 . |
Gördüğünüz gibi fcntl()
ile dosyanın bir kısmını kilitlemek mümkündür.
Dolayısıyla, flock()
sistem çağrısına göre daha fazla işlevi vardır.
Parametre olarak kilitleme yapacağı dosya isimlerini alan ve
kullanıcının Enter tuşuna basana kadar (kiliti açmak için) bekleyen
örnek bir programı ele alalım.
1 /* ex_03.c */ 2 #include <fcntl.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <sys/stat.h> 6 #include <sys/types.h> 7 #include <unistd.h> 8 9 int 10 main (int argc, char * argv []) 11 { 12 int i; 13 int fd; 14 char buffer [2]; 15 struct flock lock; 16 17 for (i = 1; i < argc; i ++) { 18 fd = open (argv [i], O_RDWR | O_CREAT, 0644); 19 if (fd < 0) { 20 fprintf (stderr, "Can't open %s\n", argv [i]); 21 exit(EXIT_FAILURE); 22 } 23 lock . l_type = F_WRLCK; 24 lock . l_whence = SEEK_SET; 25 lock . l_start = 0; 26 lock . l_len = 0; 27 if (fcntl (fd, F_SETLK, & lock) < 0) { 28 fprintf (stderr, "Can't lock %s\n", argv [i]); 29 exit(EXIT_FAILURE); 30 } 31 } 32 fprintf (stdout, "Kilitleri kaldırmak için Enter tuşuna basınız\n"); 33 fgets (buffer, 2, stdin); 34 exit(EXIT_SUCCESS); 35 }
İlk giriş ekranından (console) programı çalıştıralım :
$ cc -Wall ex_03.c -o ex_03 $ ./ex_03 myfile Kilitleri kaldırmak için Enter tuşuna basınızBaşka bir giriş ekranından da...
$ ./ex_03 myfile Can't lock myfile $İlk giriş ekranında
Enter
tuşuna basarak, kiliti açaıyoruz.
lpd
uygulamasının flock()
sistem çağrısını kullanarak
/var/lock/subsys/lpd
dosyasını sadece kendisi kullanacak şekilde
kilitlediği gibi, bu kilitleme şekli az rastlanır koşulları önlemektedir.
benzer biçimde, pam'ın fcntl()
kullanarak kullanıcı
ile ilgili /etc/passwd
dosyasındaki bilgileri güvenli
bir şekilde değiştirilebilir.
Ancak, dosya üzerinde okuma veya yazma yapmadan önce, çekirdekten gerekli erişim iznini sağlayan programlar bu şekilde davranmaktadır. Dolayısıyla biz işbirliği kilitinden söz ediyoruz demektir, yani okuma veya yazma yapmadan önce çekirdekten izin alma. Buna rağmen, bu şekilde yazılmamış bir program, başka bir süreç çekirdekten yazmak için kilit almış olsa da, dosyanın içeriğini değiştirebilir. İşte bu duruma bir örnek. Bir dosya içerisine birkaç karakter yazıp, daha önceki program ile kilitliyoruz :
$ echo "FIRST" > myfile $ ./ex_03 myfile Kilitleri kaldırmak için Enter tuşuna basınızBaşka bir giriş ekranından dosyanın içeriğini değiştirebiliriz:
$ echo "SECOND" > myfile $İlk giriş ekranına dönecek olursak, "hasarı" göreceğiz:
(Enter) $ cat myfile SECOND $
Bu sorunu çözmek için Linux çekirdeği, System V'den gelen bir kilitleme
yöntemini sistem yöneticisinin hizmetine sunmaktadır. Dolayısıyla bu yöntem,
flock()
ile değil, ancak fcntl()
ile kullanılabilir.
Sistem yöneticisi içekirdeğe, fcntl()
ile elde edilen kilitlerin
kesin olduklarını, belli erişim hakları kombinasyonu ile
söyleyebilir. Ondan sonra bir süreç bir dosya için yazmak amaçlı bir kilit
elde etmişse, herhangi başka bir süreç (root bile) dosyayı değiştiremez.
Erişim hakları kombinasyonu şöyledir : Set-GID bit aktif hale getirilirken,
dosya sahibinin ait olduğu gruptan, çalıştırma biti kaldırılır. Bunun için
aşağıdaki komut kullanılabilir :
$ chmod g+s-x myfile $Ancak, bu yeterli değildir. Bir dosya üzerine kesin kilit koyabilmek için, dosyanın bulunduğu bölme için mandatory adı verilen bir seçeneğinin belirtilmiş olması gerekmektedir. Bu genellikle,
/etc/fstab
dosyasında, mand
seçeneğini 4. sütüna eklemekle veya aşağıdaki komutları çalıştırmak ile mümkündür :
# mount /dev/hda5 on / type ext2 (rw) [...] # mount / -o remount,mand # mount /dev/hda5 on / type ext2 (rw,mand) [...] #Şimdi, başka bir giriş ekranından değişiklik yapmanın mümkün olmadığını denetleyebiliriz:
$ ./ex_03 myfile Kilitleri kaldırmak için Enter tuşuna basınızBaşka bir giriş ekranından :
$ echo "THIRD" > myfile bash: myfile: Resource temporarily not available $İlk ekrana tekrar dönelim :
(Enter) $ cat myfile SECOND $
Kesin kilit kullanılıp kullanmayacağına programcı değil, sistem yöneticisi
karar vermektedir (sözgelimi /etc/passwd
, veya /etc/shadow
dosyası için). Ortam düzgün bir şekilde ayarlandıktan sonra, dosya üzerindeki bilgilere
nasıl ulaşılacağını programcı denetlemektedir. Dolayısıyla, dosya üzerinde yazma işlemi
yapılırken, okuma işleminin tehlikeli olup olmadığına programcı karar vermektedir.
Oldukça sık rastlanan bir durum, programların disk üzerindeki dosyalarda geçici bilgi
tutması gerekmesidir. En çok rastlanan durum, bir dosyanın arasına bir yere
bilgi eklemek gerektiği zaman ortaya çıkmaktadır. Bunun için dosyanın bir kopyası,
yeni bilgiler de eklenerek, yaratılmaktadır. unlink()
sistem çağrısı,
orijinal dosyayı silmektedir ve rename()
ile geçici dosyanın adı
değiştirilmektedir.
Geçici dosya açma işlemi, eğer düzgün yapılmazsa, hasta ruhlu bir kullanıcı için, az rastlanan koşullar yaratmaktadır. Geçici dosyalar ile ilgili güvenlik açıkları yakınlarda, Apache, Linuxconf, getty_ps, wu-ftpd, rdist, gpm, inn vs gibi uygulamalarda ortaya çıkmıştır. Bu sorundan kaçınmak için, bazı noktaları hatırlatalım.
Geçici dosya açma işlemi genellikle, /tmp
dizininde yapılmaktadır.
Burasının, sistem yöneticisi için anlamı, geçici bir süreyle dosyaların tutulduğu bir yerdir.
Dolayısıyla, burasını zaman zaman temizleyen (silen), genellikle cron
da ayarlanan, programlar yazılabilir veya dizini ayrı bir bölmeye koyarak,
her sistem açılışında, yeniden biçimlendirilebilir.
Sistem yöneticisi genellikle, deçici dosyaların nerede yer alacağını,
<paths.h
> ve <stdio.h
> dosyalarındaiki
_PATH_TMP
ve P_tmpdir
simgesel sabitlerde tanımlamaktadır.
Aslında, /tmp
yerine başka bir dizin kullanmak pek akıllıca bir
davranış değildir. Nedenine gelince, bu dizini kullanan tüm programları, buna C
kütüphanesi de dahil olmak üzere, yeniden derlemek gerekecektir. Ancak,
GlibC kütüphaneleri için TMPDIR
çevre değişkeni tanımlanabilir.
Dolayısıyla, kullanıcılar, /tmp
dizini yerine, kendilerine ait dizinlerde
geçici doya yaratabilirler. Çok büyük geçici dosya kullanan uygulamalar için ve
de /tmp
için ayrılmış bölme küçük olduğunda, bu durum kaçınılmazdır.
/tmp
sistem dizini sahip olduğu erişim haklarından dolayı özel bir
yerdir :
$ ls -ld /tmp drwxrwxrwt 7 root root 31744 Feb 14 09:47 /tmp $
t
karakteri, sekizli sayı tabanına göre 01000, ile belirtilen
Sticky-Bit'in dizin üzerinde özel bir anlamı vardır. Buna göre,
sadece dizin sahibi, bu durumda root veya dosyanın sahibi, dosyayı
silebilir. Dizin içinde, sistem yöneticisinin yapacağı bir sonraki temizleme
işlemine kadar, her kullanıcı güvenli bir şekilde dosya yaratıp saklayabilir.
Ancak, geçici dosyaların konulduğu dizin beraberinde başka sorunlar getirebilir.
Sözgelimi, en basitinden, Set-UID root olan ve kullanıcı ile haberleşen bir uygulamayı
ele alalım. Elektronik ileti taşıma programını ele alalım.
Sistem kapanışı sırasında programa,
işlerini ani bir şekilde bitirmesi anlamına gelen, SIGTERM veya SIGQUIT
sinyalleri gönderilirse, program kullanıcının o anda yazmakta olduğu, ancak henüz göndermediği
e-iletisini kayıt edecektir. Programın eski sürümleri bunu /tmp/dead.letter
adı altında kayıt etmektedirler. Sıradan kullanıcılar /tmp
dizinde dosya
yaratabildikleri için, /etc/passwd
dosyasına dead.letter
adı altında bir fiziksel bağlantı oluşturup, e-ileti programlarını çalıştırarak
yazmaya devam ettikleri mesajın yerine "root::1:99999:::::
" de yer aldığı
şifre dosyasının içeriğini elde etmiş olurlar.
Bu davranışın ilk sorunu, dosya adının önceden biliniyor olmasıdır.
Programı ilk incelidiğimizde, /tmp/dead.letter
dosya adını hemen keşif edebiliyoruz. Dolayısıyla ilk yapılması gereken,
çalışan programa özgü dosya adı oluşturmak olmalıdır. Bu amaçla,
geçici ve bize özgü dosya adı oluşturan bir çok kütüphane fonksiyonu vardır.
Geçici dosya adı oluşturan böyle bir fonksiyonun olduğunu kabul edelim. Ancak, yazılım serbest olarak dağıtıldığından ve hatta C kütüphanesinin bile kaynak programlarına ulaşabildiğimizden, bizar daha zor olsa da, bu dosya adını da önceden öğrenmek mümkündür. Dolayısıyla saldırgan, C kütüphanesinin oluşturduğu dosya adı ile başka bir dosya arasında bağlantı kurabilir. Bu durumda bizim ilk tepkimiz, dosyayı açmadan önce böyle bir dosya olup olmadığını denetlemek olmalıdır. Bunu için aşağıdaki gibi bir program parçası kullanabiliriz:
if ((fd = open (filename, O_RDWR)) != -1) { fprintf (stderr, "%s already exists\n", filename); exit(EXIT_FAILURE); } fd = open (filename, O_RDWR | O_CREAT, 0644); ...
Açıktır ki böyle bir program parçası ile, az rastlanır koşulların
ortaya çıkartığı güvenlik açığı yaratmış oluruz. Kullanıcı, open()
fonksiyonunun ilk çağırılması ile ikincisi arasında, /etc/passwd
dosyasına bağlantı yaratabilir. İki işlem, aralarında başka bir işlem
girmeyecek şekilde atomik olarak veya tek olarak yapılmak zorundadır.
Bu open()
sistem çağrısına verilen özel bir seçenek ile mümkündür.
O_EXCL ve O_CREAT seçenekleri kullanıldığında fonksiyon, dosyanın var olup
olmadığını denetlemekte, varsa hata vermekte, ancak denetleme ve yaratma işlemi kesintisiz
olarak (atomik) yapılmaktadır.
Bu arada, fopen()
fonksiyonuna bir Gnu uzantısı olan 'x
'
seçeneği ile de bu işlem yapılabilmektedir :
FILE * fp; if ((fp = fopen (filename, "r+x")) == NULL) { perror ("Can't create the file."); exit (EXIT_FAILURE); }
Geçici dosyaların dosya erişim hakları da önem taşımaktadır. 644 (dosya sahibi okuyup yazabilir, diğer kullanıcılar okuyabilir) erişim haklarına sahip bir geçici dosyaya önemli bilgiler yazılması saçmalık olur.
#include <sys/types.h> #include <sys/stat.h> mode_t umask(mode_t mask);Fonksiyonu ile dosya yaratma sırasındaki erişim hakları düzenlenebilir. Böylece, fonksiyon
umask(077)
olarak çalıştırıldığında,
bundan sonra yaratılacak dosya 600 erişim haklarına (dosya sahibi okuma ve yazma,
diğer kullanıcılar hiçbir hakka sahip değil) sahip olacaktır.
Genellikle, geçici dosya yaratma işlemi üç adımdan oluşmaktadır:
O_CREAT | O_EXCL
ve mümkün en çok kısıtlanmış dosya erişim haklarıyla
dosyayı açmak.
Geçici dosyaya nasıl sahip olabiliriz?
#include <stdio.h> char *tmpnam(char *s); char *tempnam(const char *dir, const char *prefix);Fonksiyonları, rastgele yaratılmış dosya için bir işaretçi göndermektedir.
İlk fonksiyona NULL
parametresi vererek, statik bir katar adresi elde
edebiliriz. Burasının içeriği, bir sonraki tmpnam(NULL)
ile
değiştirilmektedir. Eğer parametre bir karakter katarı ise, bunun için katarın
boyutu en az L-tmpnam
byte olmalıdır, oluşturulan isim buraya
kopyalanmaktadır. Katar taşmalarına dikkat! _POSIX_THREADS
veya
_POSIX_THREAD_SAFE_FUNCTIONS
sabitleri tanımlı oldukları zaman,
oluşabilecek sorunlar hakkında fonksiyonun man
sayfasında uyarılar
vardır.
tempnam()
fonksiyonu, karakter katarına bir işaretçi
geri göndermektedir. dir
dizini "uygun" ("uygun" kelimesinin
anlamını fonksiyonun man
sayfasında anlatılmaktadır)
olarak verilmelidir. Fonksiyon dosya adını geri göndermeden önce, böyle bir
dosya olup olmadığını denetlemektedir. Ancak, fonksiyonun man
sayfasında bunun kullanımı tavsiye edilmemiştir. Fonksiyonun farklı uyarlamalarına
göre "uygun" kelimesi farklı anlamlara gelebilir.
Gnome, fonksiyonun aşağıdaki gibi kullanılmasını tavsiye etmektedir:
char *filename; int fd; do { filename = tempnam (NULL, "foo"); fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600); free (filename); } while (fd == -1);Buradaki döngü riskleri azaltmakta, ama anynı zamanda başkalarını yaratmaktadır. Geçici dosyanın yaratılacağı bölme dolu olduğu veya sistemde açılmış dosya sayısı en fazla değere ulaştığı zaman ne olacaktır?
#include <stdio.h> FILE *tmpfile (void);Fonksiyonu tekil ada sahip bir dosya yaratarak açmaktadır. Bu dosya kapanış sırasında otomatik olarak silinmektedir.
GlibC-2.1.3 ile birlikte bu fonksiyonlar tmpnam()
deki gibi bir
yöntem kullanarak dosya yaratarak açmaktadırlar. Daha sonra dosya silinmektedir,
ancak Linux, dosyayı kullanan tüm süreçler onu serbest bıraktığında, yani
close()
sistem çağrısı çalıştırıldığı zaman, dosya sistemden gerçekte
silinmektedir.
FILE * fp_tmp; if ((fp_tmp = tmpfile()) == NULL) { fprintf (stderr, "Can't create a temporary file\n"); exit (EXIT_FAILURE); } /* ... geçici dosyanın kullanımı ... */ fclose (fp_tmp); /* sistemden gerçekte silindiği yer */
Basit kullanımlarda, hatta başka sürece devredeceksek bile, dosya adına gereksinim duymayız,
sadece geçici alanı okuma ve veri saklamak için kullanırız.
Dolayısıyla, geçici dosyanın adına gereksimimiz olmamaktadır ve içeriğine ulaşmamız
yeterlidir.
tmpfile()
fonksiyonu bunu gerçekleştirmektedir.
Fonskiyonun man
sayfasında hiç birşey yazmamaktadır, ama
Secure-Programs-HOWTO nasıl dosyasında bu fonksiyonun kullanımını tavsiye etmektedir.
Yazara göre, fonskiyonun özellikleri arasında dosya yartma ile ilgli bir garanti
verilmemiştir ve kendisi tüm var olan faklı uyarlamaları denetleme firsati
bulamamıştır. Buna rağmen, bu kullanılan en etkin fonksiyondur.
Son olarak,
#include <stdlib.h> char *mktemp(char *template); int mkstemp(char *template);fonksiyonları, "
XXXXXX
" ile verilen bir modele göre
tekil isim oluşturmaktadır. Tekil dosya adı elde edilecek şekilde
'X' ler değiştirilmektedir.
mktemp()
fonksiyonun sürümlerine göre, ilk beş 'X' sürecin
numarası ile değiştirilerek, sadece bir 'X' rastgele
oluşturulmaktadır, dolayısyla geriye sadece bir karakter tahmin etmek kalıyor.
Fonksiyonun bazı sürümlerinde ise, altı adet olan 'X' lerin sayısını artırmak mümkündür.
mkstemp()
, Secure-Programs-HOWTO nasıl kaynağında tavsiye edilen
fonksiyondur. İşte kullanım yöntemi:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> void failure(msg) { fprintf(stderr, "%s\n", msg); exit(1); } /* * Geçici bir dosya yaratır ve geri gönderir. * Yarattıktan sonra, dosya silinmektedir, dolayısıyla * dizin listelendiğinde görünmemektedir. */ FILE *create_tempfile(char *temp_filename_pattern) { int temp_fd; mode_t old_mode; FILE *temp_file; /* Dosyayı kısıtlı haklar ile yarat */ old_mode = umask(077); temp_fd = mkstemp(temp_filename_pattern); (void) umask(old_mode); if (temp_fd == -1) { failure("Geçici dosya açılamadı"); } if (!(temp_file = fdopen(temp_fd, "w+b"))) { failure("Geçici dosyanın erişim numarası yaratılamadı."); } if (unlink(temp_filename_pattern) == -1) { failure("Couldn't unlink temporary file"); } return temp_file; }
Bu fonksiyonlar taşınabilirlik ile genelleştirme arasındaki sorunları
göstermektedir. Yani, standart kütüphane fonksiyonları gerekli
genelleştirmeyi sağlamaktadır, ancak onların uyarlanma şekilleri
sisteme göre farklılık göstermektedir (taşınabilirlik).
Sözgelimi, tmpfile()
fonksiyonu geçici dosyaları farklı şekilde
açmaktadır (bazı sürümler O_EXCL
seçeneğini kullanmamaktadır) veya
mkstemp()
fonksiyonunun farklı uyarlamaları 'X' leri farklı şekilde
kullanmaktadır.
Aynı kaynak kullanıldığı durumdaki az rastlanır koşullar üzerinde durduk.
İki işlemin, denetimin çekirdekte olmadığı sürece
ardışık olarak yapıldığını varsaymamak gerekir. Eğer, farklı threadlerdeki ortak
değişkenler, shmget()
ile paylaştırılan ortak bellek alanları gibi,
az rastlanır koşullar güvenlik açıkları oluşturuyorsa, başka kaynaklara dayanarak
bunları ihmal etmemek gerekir. Bulunması zor hatalardan kaçınmak için,
erişim denetleme yöntemleri (sözgelimi semaphore'lar) kullanılmalıdır.