TL; DR: Scrivu un modulu di kernel chì leghjerà cumandamenti da a carica ICMP è eseguisce nantu à u servitore ancu s'ellu u vostru SSH falla. Per i più impazienti, tuttu u codice hè github.
Avè I programatori C sperimentati rischianu di scoppia in lacrime di sangue! Puderaghju ancu esse sbagliatu in a terminologia, ma ogni critica hè benvenuta. U post hè destinatu à quelli chì anu una idea assai grossa di prugrammazione C è volenu guardà l'internu di Linux.
In i cumenti à u mo primu articulu hà citatu SoftEther VPN, chì pò imità certi protokolli "regulari", in particulare HTTPS, ICMP è ancu DNS. Puderaghju solu cum'è u primu di elli travaglià, postu chì sò assai familiarizatu cù HTTP (S), è aghju avutu à amparà tunneling over ICMP è DNS.
Iè, in 2020 aghju amparatu chì pudete inserisce una carica arbitraria in i pacchetti ICMP. Ma megliu tardu chè mai ! E postu chì qualcosa pò esse fattu per questu, allora deve esse fattu. Siccomu in a mo vita di ogni ghjornu aghju utilizatu più spessu a linea di cummanda, cumpresu via SSH, l'idea di una cunchiglia ICMP hè vinuta prima in mente. È per assemblà un bingo di bullshield cumpletu, aghju decisu di scrive cum'è un modulu Linux in una lingua chì aghju solu una idea grossa. Un tali cunchiglia ùn serà micca visibile in a lista di prucessi, pudete carricà in u kernel è ùn serà micca nantu à u sistema di schedari, ùn vi vede nunda suspettu in a lista di porti d'ascolta. In quantu à e so capacità, questu hè un rootkit cumpletu, ma spergu di migliurà è l'utilizanu cum'è una cunchiglia di l'ultimu risorsu quandu u Load Average hè troppu altu per login via SSH è eseguisce almenu. echo i > /proc/sysrq-triggerper restaurà l'accessu senza rebooting.
Pigliemu un editore di testu, cumpetenze di prugrammazione basi in Python è C, Google è virtuale chì ùn vi importa micca di mette sottu à u cuteddu se tuttu si rompe (opcional - VirtualBox / KVM / etc.) è andemu!
Latu di u cliente
Mi paria chì per a parte di u cliente avissi da scrive un script cù circa 80 linee, ma ci era persone gentili chì l'anu fattu per mè. tuttu u travagliu. U codice hè diventatu inaspettatamente simplice, chì si mette in 10 linee impurtanti:
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()
U script piglia dui argumenti, un indirizzu è una carica. Prima di mandà, a carica hè preceduta da una chjave run:, avemu bisognu per escludiri pacchetti cù carichi casuali.
U kernel hà bisognu di privilegi per creà pacchetti, cusì u script duverà esse eseguitu cum'è superuser. Ùn vi scurdate di dà permessi di esecuzione è stallà scapy stessu. Debian hà un pacchettu chjamatu python3-scapy. Avà pudete verificà cumu tuttu funziona.
Esecuzione è uscita u cumandamentu 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!
Questu hè ciò chì pare in u 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)
U payload in u pacchettu di risposta ùn cambia micca.
Modulu Kernel
Per custruisce in una macchina virtuale Debian avete bisognu di almenu make и linux-headers-amd64, u restu vinarà in forma di dipendenze. Ùn furnisce micca u codice sanu in l'articulu pudete clone nantu à Github.
Configurazione di ganciu
Per principià, avemu bisognu di duie funzioni per carricà u modulu è scaricallu. A funzione di scaricamentu ùn hè micca necessariu, ma dopu rmmod ùn hà micca travagliatu u modulu serà scaricatu solu quandu si spegne.
Dui fugliali di l'intestazione sò attirati per manipulà u modulu stessu è u netfilter.
Tutte l'operazioni passanu per un netfilter, pudete stabilisce ganci in questu. Per fà questu, avete bisognu di dichjarà a struttura in quale u ganciu serà cunfiguratu. A più impurtante hè di specificà a funzione chì serà eseguita cum'è un ganciu: nfho.hook = icmp_cmd_executor; Aghju da vene à a funzione stessu dopu.
Allora aghju stabilitu u tempu di trasfurmazioni per u pacchettu: NF_INET_PRE_ROUTING specifica di processà u pacchettu quandu appare prima in u kernel. Pò esse usatu NF_INET_POST_ROUTING per processà u pacchettu mentre esce da u kernel.
Aghju stabilitu u filtru à IPv4: nfho.pf = PF_INET;.
Dà u mo ganciu a più alta priorità: nfho.priority = NF_IP_PRI_FIRST;
È aghju registratu a struttura di dati cum'è u ganciu propiu: nf_register_net_hook(&init_net, &nfho);
A funzione finale elimina u ganciu.
A licenza hè chjaramente indicata per chì u compilatore ùn si lamenta micca.
Funzioni module_init() и module_exit() stabilisce altre funzioni per inizializà è finisce u modulu.
Recuperazione di u payload
Avà avemu bisognu di caccià a carica utile, questu hè diventatu u travagliu più difficiule. U kernel ùn hà micca funzioni integrate per travaglià cù carichi utili, pudete solu parse headers di protokolli di livellu più altu.
Aviu avutu à include i fugliali d'intestazione supplementari, sta volta per manipulà l'intestazione IP è ICMP.
Aghju stabilitu a lunghezza massima di a linea: #define MAX_CMD_LEN 1976. Perchè esattamente questu? Perchè u compilatore si lagna! Anu digià suggeritu à mè chì aghju bisognu di capiscenu a pila è u munzeddu, un ghjornu certamenti fà questu è forse ancu currettu u codice. Aghju stabilitu immediatamente a linea chì cuntene u cumandamentu: char cmd_string[MAX_CMD_LEN];. Deve esse visibile in tutte e funzioni, parleraghju di questu in più detail in u paràgrafu 9.
Avà avemu bisognu di inizializà (struct work_struct my_work;) struttura è cunnetta cù una altra funzione (DECLARE_WORK(my_work, work_handler);). Parlaraghju ancu perchè questu hè necessariu in u nuvimu paràgrafu.
Avà dichjarà una funzione chì serà un ganciu. U tipu è l'argumenti accettati sò dettati da u netfilter, ci interessa solu skb. Questu hè un buffer di socket, una struttura di dati fundamentali chì cuntene tutte l'infurmazioni dispunibili nantu à un pacchettu.
Per u funziunamentu di a funzione, avete bisognu di duie strutture è parechje variàbili, cumprese dui iteratori.
Pudemu principià cù a logica. Per u modulu per travaglià, ùn ci hè micca bisognu di pacchetti altru ch'è ICMP Echo, cusì analizemu u buffer utilizendu funzioni integrate è scaccià tutti i pacchetti non-ICMP è non-Echo. Ritorna NF_ACCEPT significa l'accettazione di u pacchettu, ma pudete ancu abbandunà i pacchetti riturnendu NF_DROP.
Ùn aghju micca pruvatu ciò chì succede senza verificà l'intestazione IP. A mo cunniscenza minima di C mi dice chì senza cuntrolli supplementari, qualcosa di terribili hè obligatu à succede. Seraghju cuntentu s'ellu mi dissuade di questu!
Avà chì u pacchettu hè di u tipu esatta chì avete bisognu, pudete estrarre i dati. Senza una funzione integrata, avete prima avè un punteru à u principiu di a carica. Questu hè fattu in un locu, avete bisognu à piglià l'indicatore à u principiu di l'intestazione ICMP è spustà à a dimensione di questu capu. Tuttu usa struttura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
A fine di l'intestazione deve currisponde à a fine di u payload in skb, dunque l'ottenemu cù i mezi nucleari da a struttura currispondente: tail = skb_tail_pointer(skb);.
A stampa hè stata arrubbata da quì, pudete leghje più nantu à u socket buffer.
Una volta avete punters à u principiu è à a fine, pudete copià e dati in una stringa cmd_string, verificate per a presenza di un prefissu run: è, o scaccià u pacchettu s'ellu manca, o riscrivite a linea di novu, cacciendu stu prefissu.
Eccu, avà pudete chjamà un altru gestore: schedule_work(&my_work);. Siccomu ùn hè micca pussibule di passà un paràmetru à una tale chjama, a linea cù u cumandamentu deve esse globale. schedule_work() metterà a funzione assuciata cù a struttura passata in a fila generale di u pianificatore di u compitu è cumpleta, chì vi permette di ùn aspittà chì u cumandamentu finisci. Questu hè necessariu perchè u ganciu deve esse assai veloce. Altrimenti, a vostra scelta hè chì nunda ùn hà da principià o avete un panicu di u kernel. U ritardu hè cum'è a morte !
Hè cusì, pudete accettà u pacchettu cù un ritornu currispundente.
Chjama un prugramma in u spaziu d'utilizatore
Sta funzione hè a più capiscibile. U so nome hè statu datu DECLARE_WORK(), u tipu è l'argumenti accettati ùn sò micca interessanti. Pigliemu a linea cù u cumandamentu è u passanu sanu à a cunchiglia. Chì ellu trattà cù l'analisi, a ricerca di binari è tuttu u restu.
Pone l'argumenti à un array di strings argv[]. Assumimu chì ognunu sapi chì i prugrammi sò veramente eseguiti in questu modu, è micca cum'è una linea cuntinua cù spazii.
Stabbilisce variabili di l'ambiente. Aghju inseritu solu PATH cù un minimu minimu di camini, sperendu chì tutti eranu digià cumminati /bin с /usr/bin и /sbin с /usr/sbin. L'altri camini raramente importanu in pratica.
Fattu, femu ! Funzione Kernel call_usermodehelper() accetta l'entrata. percorsu à u binariu, array of arguments, array of environment variables. Eccu ancu assume chì tutti capiscenu u significatu di passà a strada à u schedariu eseguibile cum'è un argumentu separatu, ma pudete dumandà. L'ultimu argumentu specifica s'ellu si deve aspittà chì u prucessu finisci (UMH_WAIT_PROC), principiu di prucessu (UMH_WAIT_EXEC) o ùn aspittà nunda (UMH_NO_WAIT). Ci hè un pocu di più UMH_KILLABLE, ùn aghju micca guardatu.
Assemblea
L'assemblea di i moduli di u kernel hè realizatu attraversu u kernel make-framework. Chjamatu make in un cartulare speciale ligatu à a versione di u kernel (definitu quì: KERNELDIR:=/lib/modules/$(shell uname -r)/build), è u locu di u modulu hè passatu à a variabile M in l'argumenti. L'icmpshell.ko è i miri puliti utilizanu stu quadru sanu. IN obj-m indica u schedariu d'ughjettu chì serà cunvertitu in un modulu. Sintassi chì rifà main.o в icmpshell.o (icmpshell-objs = main.o) ùn mi pare micca assai logicu, ma cusì sia.
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
Raccogliemu: make. Caricamentu: insmod icmpshell.ko. Fattu, pudete verificà: sudo ./send.py 45.11.26.232 "date > /tmp/test". Sì avete un schedariu nantu à a vostra macchina /tmp/test è cuntene a data chì a dumanda hè stata mandata, chì significa chì avete fattu tuttu bè è aghju fattu tuttu bè.
cunchiusioni
A mo prima sperienza cù u sviluppu nucleare hè stata assai più faciule ch'è m'aspittava. Ancu senza sperienza in u sviluppu in C, cuncintratu nantu à i suggerimenti di compilatore è i risultati di Google, aghju pussutu scrive un modulu di travagliu è sentu cum'è un pirate di kernel, è à u stessu tempu un script kiddie. Inoltre, aghju andatu à u canale Kernel Newbies, induve m'hà dettu di utilizà schedule_work() invece di chjamà call_usermodehelper() à l'internu di u ganciu stessu è l'anu vergognatu, suspettendu à ghjustizia una scam. Un centu di linee di codice mi costanu circa una settimana di sviluppu in u mo tempu liberu. Una sperienza riescita chì hà distruttu u mo mitu persunale nantu à a cumplessità eccessiva di u sviluppu di u sistema.
Se qualchissia accetta di fà una rivisione di codice in Github, vi saraghju grati. Sò sicuru ch'e aghju fattu assai sbaglii stupidi, soprattuttu quandu u travagliu cù e corde.