Ядрен шелл над ICMP

Ядрен шелл над ICMP

TL;DR: пиша модул на ядрото, който ще чете команди от ICMP полезното натоварване и ще ги изпълнява на сървъра, дори ако SSH-то ви се е скапало. За най-търпеливите, целия код на github.

Внимание! Опитните програмисти по C рискуват да се разплачат с кръвливи сълзи! Може би греша дори в терминологията, но всякаква критика е добре дошла. Постът е предназначен за тези, които имат най-общо понятие за програмиране на C и искат да се вгледат в душата на Linux.

В коментарите на моя първи статии споменаха SoftEther VPN, който може да мимикрира под някои 'обикновени' протоколи, конкретно HTTPS, ICMP и дори DNS. Представям си само работата на първия от тях, тъй като съм добре запознат с HTTP(S), а тунелирането над ICMP и DNS трябваше да опозная.

Ядрен шелл над ICMP

Да, през 2020 година научих, че в ICMP пакетите може да се вмъква произволно полезно натоварване. Но по-добре късно, отколкото никога! И след като с това може да се направи нещо, значи трябва да се действа. В ежедневието си най-често използвам командния ред, включително през SSH, и идеята за ICMP shell ми дойде на ум на първо място. И за да събера пълно булшит бинго, реших да пиша в формата на Linux модул на езика, за който имам само приблизителна представа. Такъв шел не ще бъде видим в списъка с процеси, може да бъде зареден в ядрото и няма да заема място във файловата система, няма да видите нищо подозрително в списъка с проследявани портове. По своите възможности е пълноценен rootkit, но се надявам да го доразвия и да го използвам като шел на последна надежда, когато Load Average е твърде висок, за да се свържа по SSH и да изпълня поне echo i > /proc/sysrq-trigger, за да възстановя достъпа без рестартиране.

Вземаме текстов редактор, основни умения за програмиране на Python и C, Google и виртуалната машина , която не е жалко да се пусне под нож, ако всичко се скапe (по избор - локален VirtualBox/KVM и т.н.) и да тръгваме!

Клиентска част

Мислех, че за клиентската част ще трябва да пиша скрипт от около 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 "Здравей, свят!"
Започнете емисията:
.Завършено изпращането на 1 пакета.
*
Получени 2 пакета, получили 1 отговора, оставащи 0 пакета
###[ IP ]###
версия = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
флагове =
фраг = 0
ttl = 58
протокол = icmp
проверка = 0x3403
източник = 45.11.26.232
цел = 192.168.0.240
опции
###[ ICMP ]###
тип = echo-reply
код = 0
проверка = 0xde03
id = 0x0
последователност = 0x0
###[ Raw ]###
дадени = 'run:Здравей, свят!

Така изглежда в снифера
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Работи като потребител "root" и група "root". Това може да бъде опасно.
Заснемане на 'wlp1s0'
Рамка 1: 59 байта по кабел (472 бита), 59 байта заснети (472 бита) на интерфейс wlp1s0, id 0
Интернет протокол версия 4, Източник: 192.168.0.240, Дестинация: 45.11.26.232
Протокол за контрол на интернет съобщения
Тип: 8 (Echo (ping) заявка)
Код: 0
Контролна сума: 0xd603 [коректно]
[Състояние на контролна сума: Добро]
Идентификатор (BE): 0 (0x0000)
Идентификатор (LE): 0 (0x0000)
Последователност номер (BE): 0 (0x0000)
Последователност номер (LE): 0 (0x0000)
Данни (17 байта)

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
Протокол за контрол на интернет съобщения
Type: 0 (Echo (ping) reply)
Код: 0
Checksum: 0xde03 [correct]
[Състояние на контролна сума: Добро]
Идентификатор (BE): 0 (0x0000)
Идентификатор (LE): 0 (0x0000)
Последователност номер (BE): 0 (0x0000)
Последователност номер (LE): 0 (0x0000)
[Request frame: 1]
[Response time: 19.094 ms]
Данни (17 байта)

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 са необходими поне , затова потребителите на Windows трябва да инсталират Cygwin или да се справят сами с Visual Studio и библиотеките. и linux-headers-amd64, останалото ще бъде изтеглено като зависимости. В статията няма да предоставя целия код, но можете да го клонирате от GitHub.

Настройка на хука

Първо, ще ни трябват две функции, за да заредим модула и да го изключим. Функцията за изключване не е задължителна, но в този случай 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. Два заглавни файла се изтеглят за манипулация със собствения модул и с netfilter.
  2. Всички операции преминават през netfilter, в него могат да се задават хукове. За целта трябва да се обяви структура, в която хукът ще бъде настроен. Най-важното е да се посочи функцията, която ще се изпълнява като хук: 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-фреймворк. Извиква се , затова потребителите на Windows трябва да инсталират Cygwin или да се справят сами с Visual Studio и библиотеките. вътре в специална директория, свързана с версията на ядрото (определя се тук: 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

всички: icmpshell.ko

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

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

Събираме: , затова потребителите на Windows трябва да инсталират Cygwin или да се справят сами с Visual Studio и библиотеките.. Зареждаме: insmod icmpshell.ko. Готово, може да проверите: sudo ./send.py 45.11.26.232 "date > /tmp/test". Ако на вашата машина се е появил файл /tmp/test и в него е записан дата на изпращане на заявката, означава, че всичко сте направили правилно и аз също.

Заключение

Моят първи опит с разработка на ядро се оказа много по-прост, отколкото очаквах. Дори без опит в разработката на C, базирайки се на подсказките на компилатора и резултатите от Google, успях да напиша работен модул и да се почувствам като хакер на ядрото, а също така и скрипт-кидди. Освен това влязох в канала Kernel Newbies, където ми подсказаха да използвам schedule_work() вместо извикване call_usermodehelper() в самия хук и ме упрекнаха, справедливо заподозрявайки ми шах. Сто реда код ми струваха около седмица разработка в свободното време. Успешен опит, разрушил личния ми мит за непосилната сложност на системната разработка.

Ако някой е съгласен да направи код-ревю в GitHub, ще бъда благодарен. Почти съм сигурен, че съм направил много глупави грешки, особено в работата със стрингове.

Ядрен шелл над ICMP

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster