5. Bölüm - Uygulama geliştirme sırasında güvenlik açıklarından kaçınmak : az rastlanır koşullar

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier

fr to en Georges Tarbouriech

en to tr Erdal MUTLU

AboutTheAuthor:

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.

Abstract:

Serimizin beşinci makalesinin konusu multitasking'den dolayı ortaya çıkan güvenlik sorunları ile ilgilidir. Az rastlanır koşullar altında süreçler, sadece kendilerinin eriştiğine "inandığı" kaynaklara (dosya, aygıt, bellek) aynı anda başka süreçler de erişmektedir. Bu durum, ortaya çıkarılması zor olan hatalara ve aynı zamanda sistemin genelinde güvenlik açıklarının oluşmasına neden olmaktadır.

ArticleIllustration:[illustration]

[article illustration]

ArticleBody:[The real article: put the text and html-codes here]

Giriş

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 örnek

İ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
#

Biraz daha gerçekçi olalım

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:

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.

Olası iyileştirme

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.

Genelleştirme

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..

Dosya içeriği ile ilgili az rastlanır koşullar

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ız
Baş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ız
Baş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ız
Baş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.

Geçici dosyalar

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:

  1. Tekil dosya adı (rastgele) oluşturma.
  2. O_CREAT | O_EXCL ve mümkün en çok kısıtlanmış dosya erişim haklarıyla dosyayı açmak.
  3. Dosya açma işleminin sonucunu denetleyerek, hata oluşması durumunda gerekeni yapmak (yeniden denemek veya programdan çıkmak).

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.

Sonuç

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.

Bağlantılar