Powłoka nuklearna nad ICMP

Powłoka nuklearna nad ICMP

TL; DR: Piszę moduł jądra, który będzie odczytywał polecenia z ładunku ICMP i wykonywał je na serwerze, nawet jeśli nastąpi awaria SSH. Dla najbardziej niecierpliwych cały kod jest GitHub.

Uwaga! Doświadczeni programiści C ryzykują krwawymi łzami! Mogę się nawet mylić w terminologii, ale każda krytyka jest mile widziana. Post przeznaczony jest dla tych, którzy mają bardzo ogólne pojęcie o programowaniu w C i chcą zajrzeć do Linuksa od środka.

W komentarzach do mojego pierwszego Artykuł wspomniałem o SoftEther VPN, który może naśladować niektóre „zwykłe” protokoły, w szczególności HTTPS, ICMP, a nawet DNS. Mogę sobie wyobrazić, że działa tylko pierwszy z nich, ponieważ bardzo dobrze znam protokół HTTP(S) i musiałem nauczyć się tunelowania przez ICMP i DNS.

Powłoka nuklearna nad ICMP

Tak, w 2020 roku dowiedziałem się, że do pakietów ICMP można wstawić dowolny ładunek. Ale lepiej późno niż wcale! A skoro można coś z tym zrobić, to trzeba to zrobić. Jako że na co dzień najczęściej korzystam z linii poleceń, także przez SSH, jako pierwszy przyszedł mi do głowy pomysł powłoki ICMP. Aby złożyć kompletne bingo z tarczą byczą, zdecydowałem się napisać je jako moduł Linux w języku, o którym mam tylko ogólne pojęcie. Taka powłoka nie będzie widoczna na liście procesów, możesz załadować ją do jądra i nie będzie jej w systemie plików, nie zobaczysz niczego podejrzanego na liście portów nasłuchujących. Pod względem możliwości jest to pełnoprawny rootkit, ale mam nadzieję go ulepszyć i użyć jako powłoki w ostateczności, gdy średnia obciążenia jest zbyt wysoka, aby zalogować się przez SSH i wykonać co najmniej echo i > /proc/sysrq-triggeraby przywrócić dostęp bez ponownego uruchamiania.

Bierzemy edytor tekstu, podstawową umiejętność programowania w Pythonie i C, Google i wirtualny które nie masz nic przeciwko włożeniu pod nóż, jeśli wszystko się zepsuje (opcjonalnie - lokalny VirtualBox/KVM/etc) i do dzieła!

Część klienta

Wydawało mi się, że ze strony klienta musiałbym napisać skrypt liczący około 80 linijek, ale znaleźli się mili ludzie, którzy zrobili to za mnie cała praca. Kod okazał się nieoczekiwanie prosty, mieszczący się w 10 znaczących linijkach:

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()

Skrypt przyjmuje dwa argumenty, adres i ładunek. Przed wysłaniem ładunek jest poprzedzony kluczem run:, będziemy go potrzebować, aby wykluczyć pakiety z losowymi ładunkami.

Jądro wymaga uprawnień do tworzenia pakietów, więc skrypt będzie musiał zostać uruchomiony jako superużytkownik. Nie zapomnij nadać uprawnień do wykonywania i zainstalować samego scapy. Debian ma pakiet o nazwie python3-scapy. Teraz możesz sprawdzić jak to wszystko działa.

Uruchamianie i wysyłanie polecenia
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!

Tak to wygląda w snifferze
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

Ładunek w pakiecie odpowiedzi nie zmienia się.

Moduł jądra

Aby zbudować maszynę wirtualną Debiana, będziesz potrzebować co najmniej make и linux-headers-amd64, reszta przyjdzie w formie zależności. Nie będę podawać całego kodu w artykule, możesz go sklonować na Githubie.

Konfiguracja haka

Na początek potrzebujemy dwóch funkcji, aby załadować moduł i go rozładować. Funkcja rozładunku nie jest wymagana, ale wtedy rmmod to nie zadziała, moduł zostanie rozładowany dopiero po wyłączeniu.

#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);

Co tu się dzieje:

  1. Wciągane są dwa pliki nagłówkowe w celu manipulowania samym modułem i filtrem sieciowym.
  2. Wszystkie operacje przechodzą przez netfilter, możesz w nim ustawić hooki. Aby to zrobić należy zadeklarować strukturę w której zostanie skonfigurowany hook. Najważniejsze jest określenie funkcji, która będzie wykonywana jako hook: nfho.hook = icmp_cmd_executor; Do samej funkcji przejdę później.
    Następnie ustalam czas realizacji paczki: NF_INET_PRE_ROUTING określa przetwarzanie pakietu, gdy po raz pierwszy pojawi się w jądrze. Może być użyte NF_INET_POST_ROUTING do przetwarzania pakietu wychodzącego z jądra.
    Ustawiłem filtr na IPv4: nfho.pf = PF_INET;.
    Daję mojemu hakowi najwyższy priorytet: nfho.priority = NF_IP_PRI_FIRST;
    I rejestruję strukturę danych jako rzeczywisty hak: nf_register_net_hook(&init_net, &nfho);
  3. Ostatnia funkcja usuwa hak.
  4. Licencja jest wyraźnie wskazana, aby kompilator nie narzekał.
  5. funkcje module_init() и module_exit() ustaw inne funkcje inicjujące i kończące moduł.

Pobieranie ładunku

Teraz musimy wydobyć ładunek, co okazało się najtrudniejszym zadaniem. Jądro nie ma wbudowanych funkcji do pracy z ładunkami; można jedynie analizować nagłówki protokołów wyższego poziomu.

#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;
}

Co się dzieje:

  1. Musiałem dołączyć dodatkowe pliki nagłówkowe, tym razem w celu manipulowania nagłówkami IP i ICMP.
  2. Ustawiłem maksymalną długość linii: #define MAX_CMD_LEN 1976. Dlaczego właśnie to? Ponieważ kompilator na to narzeka! Już mi zasugerowali, że muszę zrozumieć stos i stertę, kiedyś na pewno to zrobię i może nawet poprawię kod. Natychmiast ustawiam linię, która będzie zawierać polecenie: char cmd_string[MAX_CMD_LEN];. Powinno być widoczne we wszystkich funkcjach, o czym szerzej napiszę w paragrafie 9.
  3. Teraz musimy zainicjować (struct work_struct my_work;) strukturę i połączyć ją z inną funkcją (DECLARE_WORK(my_work, work_handler);). O tym, dlaczego jest to konieczne, porozmawiam również w akapicie dziewiątym.
  4. Teraz deklaruję funkcję, która będzie hakiem. Typ i akceptowane argumenty są podyktowane przez netfilter, nas tylko interesuje skb. Jest to bufor gniazda, podstawowa struktura danych zawierająca wszystkie dostępne informacje o pakiecie.
  5. Aby funkcja działała potrzebne będą dwie struktury i kilka zmiennych, w tym dwa iteratory.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Możemy zacząć od logiki. Aby moduł działał, nie są potrzebne żadne pakiety inne niż ICMP Echo, dlatego analizujemy bufor za pomocą wbudowanych funkcji i wyrzucamy wszystkie pakiety inne niż ICMP i inne niż Echo. Powrót NF_ACCEPT oznacza przyjęcie przesyłki, ale możesz też zrezygnować z przesyłki poprzez zwrot 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;
      }

    Nie testowałem, co się stanie bez sprawdzenia nagłówków IP. Moja minimalna znajomość C mówi mi, że bez dodatkowych kontroli wydarzy się coś strasznego. Będzie mi miło, jeśli mnie od tego odwiedziesz!

  7. Teraz, gdy pakiet jest dokładnie takiego typu, jakiego potrzebujesz, możesz wyodrębnić dane. Bez wbudowanej funkcji najpierw musisz uzyskać wskaźnik na początek ładunku. Odbywa się to w jednym miejscu, należy przenieść wskaźnik na początek nagłówka ICMP i przesunąć go do rozmiaru tego nagłówka. Wszystko używa struktury icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Koniec nagłówka musi odpowiadać końcowi ładunku skb, dlatego otrzymujemy go za pomocą środków nuklearnych z odpowiedniej struktury: tail = skb_tail_pointer(skb);.

    Powłoka nuklearna nad ICMP

    Obraz został skradziony stąd, możesz przeczytać więcej o buforze gniazda.

  8. Gdy masz już wskaźniki na początek i koniec, możesz skopiować dane do ciągu znaków cmd_string, sprawdź obecność przedrostka run: i albo odrzuć pakiet, jeśli go brakuje, albo przepisz linię ponownie, usuwając ten przedrostek.
  9. To wszystko, teraz możesz zadzwonić do innego handlera: schedule_work(&my_work);. Ponieważ do takiego wywołania nie będzie możliwości przekazania parametru, linia z poleceniem musi być globalna. schedule_work() umieści funkcję związaną z przekazaną strukturą w kolejce ogólnej harmonogramu zadań i zakończy, dzięki czemu nie będziesz czekać na zakończenie polecenia. Jest to konieczne, ponieważ hak musi być bardzo szybki. W przeciwnym razie możesz wybrać, że nic się nie uruchomi lub wystąpi panika jądra. Opóźnienie jest jak śmierć!
  10. To wszystko, możesz przyjąć paczkę z odpowiednim zwrotem.

Wywoływanie programu w przestrzeni użytkownika

Ta funkcja jest najbardziej zrozumiała. Podana została jego nazwa DECLARE_WORK(), typ i akceptowane argumenty nie są interesujące. Bierzemy linię z poleceniem i przekazujemy ją w całości do powłoki. Niech zajmie się parsowaniem, wyszukiwaniem plików binarnych i całą resztą.

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. Ustaw argumenty na tablicę ciągów argv[]. Zakładam, że każdy wie, że programy faktycznie są wykonywane w ten sposób, a nie jako ciągła linia ze spacjami.
  2. Ustaw zmienne środowiskowe. Wstawiłem tylko PATH z minimalnym zestawem ścieżek, mając nadzieję, że wszystkie zostały już połączone /bin с /usr/bin и /sbin с /usr/sbin. Inne ścieżki rzadko mają znaczenie w praktyce.
  3. Gotowe, zróbmy to! Funkcja jądra call_usermodehelper() akceptuje wpis. ścieżka do pliku binarnego, tablica argumentów, tablica zmiennych środowiskowych. Tutaj też zakładam, że każdy rozumie znaczenie przekazania ścieżki do pliku wykonywalnego jako osobnego argumentu, ale można zapytać. Ostatni argument określa, czy czekać na zakończenie procesu (UMH_WAIT_PROC), rozpoczęcie procesu (UMH_WAIT_EXEC) lub w ogóle nie czekać (UMH_NO_WAIT). Czy jest jeszcze coś? UMH_KILLABLE, nie zagłębiałem się w to.

montaż

Montaż modułów jądra odbywa się poprzez strukturę tworzącą jądro. Zwany make w specjalnym katalogu powiązanym z wersją jądra (zdefiniowanym tutaj: KERNELDIR:=/lib/modules/$(shell uname -r)/build), a lokalizacja modułu jest przekazywana do zmiennej M w argumentach. Icmpshell.ko i clean targets w całości korzystają z tego frameworka. W obj-m wskazuje plik obiektowy, który zostanie przekonwertowany na moduł. Składnia, która zmienia main.o в icmpshell.o (icmpshell-objs = main.o) nie wygląda mi to zbyt logicznie, ale niech tak będzie.

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

Zbieramy: make. Ładowanie: insmod icmpshell.ko. Gotowe, możesz sprawdzić: sudo ./send.py 45.11.26.232 "date > /tmp/test". Jeśli masz plik na swoim komputerze /tmp/test i zawiera datę wysłania prośby, co oznacza, że ​​zrobiłeś wszystko dobrze i ja zrobiłem wszystko dobrze.

wniosek

Moje pierwsze doświadczenie z rozwojem energetyki jądrowej było znacznie łatwiejsze, niż się spodziewałem. Nawet bez doświadczenia w programowaniu w C, skupiając się na wskazówkach kompilatora i wynikach Google, udało mi się napisać działający moduł i poczuć się jak hacker jądra, a jednocześnie dzieciak skryptowy. Dodatkowo udałem się na kanał Kernel Newbies, gdzie kazano mi skorzystać schedule_work() zamiast dzwonić call_usermodehelper() wewnątrz samego haka i zawstydził go, słusznie podejrzewając oszustwo. Sto linii kodu kosztowało mnie około tygodnia programowania w wolnym czasie. Udane doświadczenie, które zniszczyło mój osobisty mit o przytłaczającej złożoności rozwoju systemów.

Jeśli ktoś zgodzi się na recenzję kodu na Githubie, będę wdzięczny. Jestem pewien, że popełniłem wiele głupich błędów, szczególnie podczas pracy ze strunami.

Powłoka nuklearna nad ICMP

Źródło: www.habr.com

Dodaj komentarz