Jaderný plášť nad ICMP

Jaderný plášť nad ICMP

TL, DR: Píšu modul jádra, který bude číst příkazy z datové části ICMP a provádět je na serveru, i když vaše SSH selže. Pro ty netrpělivé je celý kód GitHub.

Pozor! Zkušení programátoři C riskují, že propuknou krvavé slzy! Možná se mýlím i v terminologii, ale každá kritika je vítána. Příspěvek je určen pro ty, kteří mají velmi hrubou představu o programování v C a chtějí nahlédnout do nitra Linuxu.

V komentářích k mému prvnímu článek zmíněná SoftEther VPN, která dokáže napodobit některé „běžné“ protokoly, zejména HTTPS, ICMP a dokonce DNS. Umím si představit, že funguje pouze první z nich, protože HTTP(S) velmi dobře znám a musel jsem se naučit tunelovat přes ICMP a DNS.

Jaderný plášť nad ICMP

Ano, v roce 2020 jsem se dozvěděl, že do paketů ICMP můžete vložit libovolný náklad. Ale lepší pozdě než nikdy! A protože se s tím dá něco dělat, tak je potřeba to udělat. Protože v každodenním životě nejčastěji používám příkazový řádek, a to i přes SSH, jako první mě napadla myšlenka shellu ICMP. A abych sestavil kompletní bullshield bingo, rozhodl jsem se to napsat jako linuxový modul v jazyce, o kterém mám jen hrubou představu. Takový shell nebude vidět v seznamu procesů, můžete ho načíst do jádra a nebude v souborovém systému, v seznamu naslouchacích portů neuvidíte nic podezřelého. Z hlediska jeho schopností se jedná o plnohodnotný rootkit, ale doufám, že jej vylepším a použiji jako shell poslední záchrany, když je Load Average příliš vysoký na přihlášení přes SSH a spuštění min. echo i > /proc/sysrq-triggerobnovit přístup bez restartu.

Bereme textový editor, základní programovací dovednosti v Pythonu a C, Google a virtuální který vám nevadí dát pod nůž, pokud se vše rozbije (volitelně - místní VirtualBox/KVM/atd) a jdeme na to!

Klientská část

Zdálo se mi, že pro klientskou část budu muset napsat scénář s asi 80 řádky, ale našli se laskaví lidé, kteří to udělali za mě veškerou práci. Kód se ukázal jako nečekaně jednoduchý a vešel se do 10 významných řádků:

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

Skript používá dva argumenty, adresu a užitečné zatížení. Před odesláním předchází užitečné zatížení klíč run:, budeme jej potřebovat k vyloučení balíčků s náhodným zatížením.

Jádro vyžaduje oprávnění k vytváření balíčků, takže skript bude muset být spuštěn jako superuživatel. Nezapomeňte udělit oprávnění ke spuštění a nainstalovat samotné scapy. Debian má balíček s názvem python3-scapy. Nyní můžete zkontrolovat, jak to celé funguje.

Spuštění a výstup příkazu
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!

Takhle to vypadá ve snifferu
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

Užitná zátěž v balíčku odpovědí se nemění.

Modul jádra

K zabudování virtuálního stroje Debian budete potřebovat min make и linux-headers-amd64, zbytek přijde ve formě závislostí. Nebudu poskytovat celý kód v článku, můžete jej naklonovat na Github.

Nastavení háku

Pro začátek potřebujeme dvě funkce, abychom modul mohli načíst a uvolnit. Funkce pro vykládání není vyžadována, ale pak rmmod nebude fungovat, modul se vyjme pouze při vypnutí.

#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 se tam děje:

  1. Pro manipulaci se samotným modulem a síťovým filtrem jsou vtaženy dva soubory záhlaví.
  2. Všechny operace procházejí přes síťový filtr, můžete v něm nastavit háčky. Chcete-li to provést, musíte deklarovat strukturu, ve které bude háček nakonfigurován. Nejdůležitější je určit funkci, která bude provedena jako hák: nfho.hook = icmp_cmd_executor; K samotné funkci se dostanu později.
    Poté nastavím dobu zpracování balíčku: NF_INET_PRE_ROUTING určuje zpracování balíčku, když se poprvé objeví v jádře. Může být použito NF_INET_POST_ROUTING zpracovat paket při jeho výstupu z jádra.
    Nastavil jsem filtr na IPv4: nfho.pf = PF_INET;.
    Svému háčku dávám nejvyšší prioritu: nfho.priority = NF_IP_PRI_FIRST;
    A registruji datovou strukturu jako skutečný háček: nf_register_net_hook(&init_net, &nfho);
  3. Poslední funkce odstraní háček.
  4. Licence je jasně označena, aby si kompilátor nestěžoval.
  5. funkce module_init() и module_exit() nastavit další funkce pro inicializaci a ukončení modulu.

Načítání užitečného zatížení

Nyní potřebujeme extrahovat užitečné zatížení, to se ukázalo jako nejtěžší úkol. Jádro nemá vestavěné funkce pro práci s datovými částmi, můžete analyzovat pouze hlavičky protokolů vyšší úrovně.

#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 se děje:

  1. Musel jsem zahrnout další soubory hlaviček, tentokrát pro manipulaci s hlavičkami IP a ICMP.
  2. Nastavil jsem maximální délku řádku: #define MAX_CMD_LEN 1976. Proč přesně tohle? Protože si na to kompilátor stěžuje! Už mi naznačili, že musím rozumět zásobníku a hromadě, někdy to určitě udělám a možná i opravím kód. Okamžitě jsem nastavil řádek, který bude obsahovat příkaz: char cmd_string[MAX_CMD_LEN];. Mělo by být viditelné ve všech funkcích; podrobněji o tom budu mluvit v odstavci 9.
  3. Nyní musíme inicializovat (struct work_struct my_work;) strukturovat a propojit ji s jinou funkcí (DECLARE_WORK(my_work, work_handler);). O tom, proč je to nutné, budu také mluvit v devátém odstavci.
  4. Nyní deklaruji funkci, která bude háčkem. Typ a přijímané argumenty jsou diktovány síťovým filtrem, nás pouze zajímá skb. Toto je vyrovnávací paměť soketu, základní datová struktura, která obsahuje všechny dostupné informace o paketu.
  5. Aby funkce fungovala, budete potřebovat dvě struktury a několik proměnných, včetně dvou iterátorů.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Můžeme začít logikou. Aby modul fungoval, nejsou potřeba žádné jiné pakety než ICMP Echo, takže analyzujeme vyrovnávací paměť pomocí vestavěných funkcí a vyhodíme všechny ne-ICMP a non-Echo pakety. Vrátit se NF_ACCEPT znamená převzetí balíku, ale balíky můžete odevzdat i vrácením 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;
      }

    Netestoval jsem, co se stane bez kontroly IP hlaviček. Moje minimální znalost C mi říká, že bez dalších kontrol se nutně stane něco hrozného. Budu rád, když mě od toho odradíte!

  7. Nyní, když je balíček přesně toho typu, jaký potřebujete, můžete extrahovat data. Bez vestavěné funkce musíte nejprve získat ukazatel na začátek užitečného zatížení. To se provádí na jednom místě, je třeba vzít ukazatel na začátek ICMP hlavičky a přesunout ji na velikost této hlavičky. Vše využívá strukturu icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Konec hlavičky se musí shodovat s koncem užitečného zatížení v skb, proto jej získáme pomocí jaderných prostředků z odpovídající struktury: tail = skb_tail_pointer(skb);.

    Jaderný plášť nad ICMP

    Obraz byl ukraden proto, můžete si přečíst více o vyrovnávací paměti soketu.

  8. Jakmile budete mít ukazatele na začátek a konec, můžete data zkopírovat do řetězce cmd_string, zkontrolujte, zda neobsahuje předponu run: a buď balíček zahoďte, pokud chybí, nebo přepište řádek znovu a odstraňte tuto předponu.
  9. To je vše, nyní můžete zavolat jiného handlera: schedule_work(&my_work);. Protože takovému volání nebude možné předat parametr, musí být řádek s příkazem globální. schedule_work() zařadí funkci spojenou s předávanou strukturou do obecné fronty plánovače úloh a dokončí ji, což vám umožní čekat na dokončení příkazu. To je nutné, protože háček musí být velmi rychlý. V opačném případě je vaše volba, že se nic nespustí, nebo dostanete jadernou paniku. Zpoždění je jako smrt!
  10. To je vše, balíček můžete přijmout s odpovídajícím vrácením.

Volání programu v uživatelském prostoru

Tato funkce je nejsrozumitelnější. Jeho jméno bylo uvedeno v DECLARE_WORK(), typ a přijaté argumenty nejsou zajímavé. Vezmeme řádek s příkazem a předáme jej celý do shellu. Ať se zabývá parsováním, hledáním binárních souborů a vším ostatním.

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. Nastavte argumenty na pole řetězců argv[]. Budu předpokládat, že každý ví, že programy se skutečně spouštějí tímto způsobem a ne jako souvislá čára s mezerami.
  2. Nastavte proměnné prostředí. Vložil jsem pouze PATH s minimální sadou cest a doufal jsem, že jsou všechny již zkombinované /bin с /usr/bin и /sbin с /usr/sbin. Jiné cesty v praxi málokdy hrají roli.
  3. Hotovo, jdeme na to! Funkce jádra call_usermodehelper() přijímá vstup. cesta k binárnímu souboru, pole argumentů, pole proměnných prostředí. Zde také předpokládám, že každý chápe význam předání cesty ke spustitelnému souboru jako samostatného argumentu, ale ptát se můžete. Poslední argument určuje, zda se má čekat na dokončení procesu (UMH_WAIT_PROC), zahájení procesu (UMH_WAIT_EXEC) nebo nečekat vůbec (UMH_NO_WAIT). Je tam ještě nějaké UMH_KILLABLE, nedíval jsem se na to.

shromáždění

Sestavení modulů jádra se provádí prostřednictvím rámce pro tvorbu jádra. Volal make uvnitř speciálního adresáře svázaného s verzí jádra (definováno zde: KERNELDIR:=/lib/modules/$(shell uname -r)/build) a umístění modulu je předáno proměnné M v argumentech. icmpshell.ko a čisté cíle využívají výhradně tento rámec. V obj-m označuje soubor objektu, který bude převeden na modul. Syntaxe, která předělává main.o в icmpshell.o (icmpshell-objs = main.o) mi nepřijde moc logické, ale budiž.

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

Sbíráme: make. Načítání: insmod icmpshell.ko. Hotovo, můžete zkontrolovat: sudo ./send.py 45.11.26.232 "date > /tmp/test". Pokud máte v počítači soubor /tmp/test a obsahuje datum odeslání žádosti, což znamená, že jste udělali vše správně a já udělal vše správně.

Závěr

Moje první zkušenost s jaderným vývojem byla mnohem jednodušší, než jsem čekal. I bez zkušeností s vývojem v C, se zaměřením na rady kompilátoru a výsledky Google, jsem byl schopen napsat funkční modul a připadal jsem si jako hacker jádra a zároveň dítě se skripty. Kromě toho jsem šel na kanál Kernel Newbies, kde mi bylo řečeno, abych to použil schedule_work() místo volání call_usermodehelper() uvnitř samotného háku a zahanbil ho, oprávněně měl podezření na podvod. Sto řádků kódu mě stálo asi týden vývoje ve volném čase. Úspěšná zkušenost, která zničila můj osobní mýtus o ohromné ​​složitosti vývoje systému.

Pokud někdo souhlasí s provedením kontroly kódu na Github, budu vděčný. Jsem si docela jistý, že jsem udělal spoustu hloupých chyb, zvláště při práci se strunami.

Jaderný plášť nad ICMP

Zdroj: www.habr.com

Přidat komentář