TL;DR: kirjutan tuumamoodulit, mis loeb ICMP-payload'ist käske ja täidab neid serveris isegi siis, kui teie SSH kukub alla. Kodeerimine on kogu kodeeringu jaoks .
Olge ettevaatlik! Kogenud C-programmeerijad võivad nutta veretuks pisarateks! Võin eksida isegi terminoloogias, kuid kõik kriitika on teretulnud. Postitus on suunatud neile, kes on programmeerimisest C keeles vaid maamärkide järgi kuulnud ja soovivad piiluda Linuxi sisemusse.
Minu esimeses kommentaaris mainiti SoftEther VPN-i, mis suudab peituda mõnede 'tavaliste' protokollide, sealhulgas HTTPS-i, ICMP-i ja isegi DNS-i alla. Kujutlen ainult esimeselt töötamist, kuna olen hoolsalt tuttav HTTP(S)-ga, ja ICMP- ja DNS-i kaudu tunneldamine tuli mul õppida.

Jah, ma sain 2020. aastal teada, et ICMP-pakettidesse saab sisestada suvalisi koormusi. Kuid parem hilja kui mitte kunagi! Ja kuna sellega saab midagi teha, tulebki seda teha. Oma igapäevases elus kasutan ma kõige sagedamini käsuread, sealhulgas SSH kaudu, seega tuli idee ICMP-shelle kõigepealt meelde. Ja et saavutada täielik bullshiti bingo, otsustasin kirjutada selle Linuxi moodulina keeles, millest mul on vaid umbkaudne ettekujutus. Selline shell ei ole nähtav protsesside nimekirjas, seda saab laadida tuuma ja see ei asu failisüsteemis, te ei näe kahtlasi asju kuulamisportide nimekirjas. Oma võimete poolest on see täielik rootkit, kuid ma loodan seda täiustada ja kasutada viimase lootuse shelle, kui koormus on liiga kõrge, et SSH kaudu sisse logida ja vähemalt echo i > /proc/sysrq-trigger, et taastada ligipääs ilma taaskäivitamiseta.
Võtame tekstiredaktori, põhioskused Pythonis ja C-s, Google'i ja utili, mida pole kahju vajadusel hukka saata (valikuline — kohalik VirtualBox / KVM / jne) ja hakkame pihta!
Kliendi osa
Eeldasin, et kliendiosale tuleb kirjutada umbes 80 rida skripti, kuid head inimesed tegid selle minu eest . Kood osutus ootamatult lihtsaks, mahub 10 olulisse ritta:
import sys
from scapy.all import sr1, IP, ICMP
if len(sys.argv) < 3:
print('Kasutus: {} 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 võtab vastu kaks argumenti: aadress ja payload. Enne saatmist eelnen payload võtmega run:, meil on seda vaja, et välistada pakette juhusliku payloadiga.
Kernel vajab õigusi pakettide koostamiseks, seega tuleb skript käivitada administraatori õigustes. Ära unusta anda käitamisõigused ja installida scapy. Debiani süsteemis on pakett nimega python3-scapy. Nüüd saab kontrollida, kuidas see kõik töötab.
Käivitamine ja käsu väljund
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Tere, maailm!"
Käivita emissioon:
.Lõpetatud 1 paketi saatmine.
*
Saadud 2 paketti, 1 vastust, 0 paketti jääb
###[ IP ]###
versioon = 4
ihl = 5
tos = 0x0
pikkus = 45
id = 17218
lipud =
frag = 0
ttl = 58
proto = icmp
chk = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
options
###[ ICMP ]###
tüüp = echo-vastus
kood = 0
chk = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
koormus = 'jooksuta:Tere, maailm!
Nii see välja näeb snifferis
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Käitatakse kasutajana "root" ja grupis "root". See võib olla ohtlik.
Püüdmine toimub 'wlp1s0' peal
Raam 1: 59 baiti traadis (472 bitti), 59 baiti püütud (472 bitti) liidesel wlp1s0, id 0
Internetiprotokoll Versioon 4, Src: 192.168.0.240, Dst: 45.11.26.232
Interneti kontrollsõnumi protokoll
Tüüp: 8 (Echo (ping) päring)
Kood: 0
Kontrollsumma: 0xd603 [õige]
[Kontrollsumma olek: Hea]
Identifikaator (BE): 0 (0x0000)
Identifikaator (LE): 0 (0x0000)
Järjekorra number (BE): 0 (0x0000)
Järjekorra number (LE): 0 (0x0000)
Andmed (17 baiti)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Pikkus: 17]
Raam 2: 59 baiti juhtmes (472 bitti), 59 baiti salvestatud (472 bitti) liidese wlp1s0 peal, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Interneti kontrollsõnumi protokoll
Tüüp: 0 (Echo (ping) vastus)
Kood: 0
Kontrollsumma: 0xde03 [õige]
[Kontrollsumma olek: Hea]
Identifikaator (BE): 0 (0x0000)
Identifikaator (LE): 0 (0x0000)
Järjekorra number (BE): 0 (0x0000)
Järjekorra number (LE): 0 (0x0000)
[Päringu raam: 1]
[Vastuse aeg: 19.094 ms]
Andmed (17 baiti)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Pikkus: 17]
^C2 paketti salvestati
Payload vastuspaketis ei muutu.
Kernelimoodul
Для сборки в виртуалке с Debian понадобятся как минимум make ja linux-headers-amd64, остальное подтянется в виде зависимостей. В статье код целиком приводить не буду, вы его можете склонировать на гитхабе.
Настройка хука
Для начала нам понадобятся две функции для того, чтобы загрузить модуль и чтобы его выгрузить. Функция для выгрузки не обязательна, но тогда и rmmod выполнить не получится, модуль выгрузится только при выключении.
#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);Что здесь происходит:
- Подтягиваются два заголовочных файла для манипуляций собственно с модулем и с нетфильтром.
- Все операции проходят через нетфильтр, в нём можно задавать хуки. Для этого нужно заявить структуру, в которой хук будет настраиваться. Самое важное — указать функцию, которая будет выполняться в качестве хука:
nfho.hook = icmp_cmd_executor;до самой функции я ещё доберусь.
Затем я задал момент обработки пакета:NF_INET_PRE_ROUTINGуказывает обрабатывать пакет, когда он только появился в ядре. Можно использоватьNF_INET_POST_ROUTINGдля обработки пакета на выходе из ядра.
Вешаю фильтр на IPv4:nfho.pf = PF_INET;.
Назначаю своему хуку наивысшей приоритет:nfho.priority = NF_IP_PRI_FIRST;
Ja registreerin andmestruktuuri kui tegelikult hübriid:nf_register_net_hook(&init_net, &nfho); - Lõppfunktsioonis hübriid eemaldatakse.
- Litsents on selgelt määratletud, et kompilaator ei nuriseks.
- Funktsioonid
module_init()jamodule_exit()moodustavad teised funktsioonid mooduli alguse ja lõpetamise.
Payloadi väljavõtt
Nüüd on vaja ekstraktida payload, see osutus kõige keerulisemaks ülesandeks. Süsteemis ei ole sisseehitatud funktsioone payloadiga töötamiseks, saab ainult analüüsida kõrgema taseme protokollide pealkirju.
#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;
}Mis toimub:
- Pidin lisama täiendavaid pealkirju, seekord IP- ja ICMP-pealkirjade manipuleerimiseks.
- Määran maksimaalse rea pikkuse:
#define MAX_CMD_LEN 1976. Miks just selline? Sest suurema pikkuse puhul nuriseb kompilaator! Keegi on juba soovitanud mul tegeleda virna ja kuhjaga, kunagi kindlasti teen seda ja võib-olla isegi parandan koodi. Otse määran rea, kuhu paigutatakse käsk:char cmd_string[MAX_CMD_LEN];. See peab olema nähtav kõigis funktsioonides, sellest räägin põhjalikumalt punktis 9. - Nüüd tuleb initsialiseerida (
struct work_struct my_work;) struktuur ja siduda see veel ühe funktsiooniga (DECLARE_WORK(my_work, work_handler);). Räägin ka sellest, miks see vajalik on, üheksandas punktis. - Nüüd kuulutan välja funktsiooni, mis ongi hook. Tüüp ja vastuvõetavad argumendid dikteeritakse netfiltri poolt, meid huvitab ainult
skb. See on soketibufar, põhistruktuur, mis sisaldab kõiki saadaval olevaid andmeid paketi kohta. - Funktsiooni töö jaoks on vaja kahte struktuuri ja mõnda muutujat, sealhulgas kahte iteraatorit.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - Võime jätkata loogikaga. Mooduli tööks pole vaja muid pakette peale ICMP Echo, seega parsim bufri sisseehitatud funktsioonide abil ja viskame kõik mitte-ICMP ja mitte-Echo paketid välja. Tagastamine
NF_ACCEPTtähendab paketi aktsepteerimist, kuid võite ka paketid ära visata, tagastadesNF_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; }Ma ei ole kontrollinud, mis juhtub ilma IP-peade kontrollita. Minu minimaalne C-keele teadmine ütleb, et ilma täiendavate kontrollideta juhtub kindlasti midagi kohutavat. Mul oleks hea meel, kui te mind selles veenaksite!
- Nüüd, kui pakett on täpselt õige, saab andmeid välja võtta. Ilma sisseehitatud funktsioonita tuleb esmalt saada osutaja koormuse algusele. See toimub läbi ühe koha, tuleb võtta osutaja ICMP päise algusele ja nihutada see selle päise suuruse võrra. Kõike seda kasutatakse struktuuri kaudu.
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
Päise lõpp peab langema kasuliku koormuse lõpu pealeskb, seega saame selle tuumimeetodite abil vastavast struktuurist:tail = skb_tail_pointer(skb);.
Pildi tõin ära , võite lugeda rohkem socket’i puhasti kohta. - Saades osutajad algusele ja lõppu, saab andmed kopeerida stringi
cmd_string, kontrollida selle olemasolu eelneva tähegarun:ja kas visata pakett välja, kui see puudub, või kirjutada string uuesti, eemaldades selle eelneva tähe. - Noh, nüüd saab kutsuda veel ühe käsitleja:
schedule_work(&my_work);. Kuna korralduseks ei saa parameetrit edastada, peab käsk string olema globaalne.schedule_work()paigutab funktsiooni, mis on seotud edastatud struktuuriga, ülesande ajakava üldisse järjekorda ja lõpeb, võimaldades oota käsu täitmist. See on vajalik, kuna hook peab olema väga kiire. Vastasel juhul ei käivitu midagi või saate kernel panic'i. Iga viivitus võib olla saatuslik! - Kõik, saate vastava tagastusega paki vastu võtta.
Programmi käivitamine kasutusruumis
See funktsioon on kõige arusaadavam. Selle nimi on määratud DECLARE_WORK(), tüüp ja vastuvõetavad argumendid ei huvita. Võtame käsurea ja edastame selle täielikult shellile. Las ta lahendab parsing'u, binaaride otsimise ja kõik muu.
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);
}- Seame argumentidega stringi massiivi
argv[]. Eeldan, et kõik teavad, et programmid tegelikult täidetakse just nii, mitte lihtsalt pideva stringina ruumide vahel. - Seame keskkonnamuutujad. Panin ainult PATH minimeeritud teede kogumiga, lootes, et kõigil on need juba ühendatud
/binkoos/usr/binja/sbinkoos/usr/sbin. Muud teed on praktikas üsna harva olulised. - Valmis, käivitame! Kerneli funktsioon
call_usermodehelper()võtab sisendiks failitee, argumentide massiivi ja keskkonna muutuja massiivi. Siin ma eeldan ka, et kõik saavad aru, et täitevfaili tee edastamine eraldi argumendina on vajalik, aga võite küsida. Viimane argument näitab, kas oodata protsessi lõpetamist (UMH_WAIT_PROC), protsessi käivitamist (UMH_WAIT_EXEC) või mitte oodata üldse (UMH_NO_WAIT). Veel on olemasUMH_KILLABLE, aga ma ei hakanud sellega süvitsi minema.
Koostamine
Kerneli moodulite koostamine toimub sama kerneli make-raamistiku kaudu. See kutsub esile make spetsiaalses kaustas, mis on seotud kerneli versiooniga (määratakse siit: KERNELDIR:=/lib/modules/$(shell uname -r)/build), ja mooduli asukoht edastatakse muutujana M argumentides. Icmpshell.ko ja clean sihiks kasutatakse täielikult seda raamistiku. obj-m määrab objekti fail, mis muudetakse mooduliks. Süntaks, mis muudab main.o ühes icmpshell.o (icmpshell-objs = main.o) tundub mulle mitte väga loogiline, aga las olla.
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
Koostame: make. Laadime: insmod icmpshell.ko. Valmis, saab kontrollida: sudo ./send.py 45.11.26.232 "date > /tmp/test". Kui teie masinas on välja tekkinud fail /tmp/test ja, ning selles on päringu saatmise kuupäev, mis tähendab, et te tegite kõik õigesti ja ma tegin kõik õigesti.
Kokkuvõte
Minu esimene kogemus tuumaarenduses oli palju lihtsam, kui ma ootasin. Isegi ilma C arenduskogemuseta, tuginedes kompilaatori vihjetele ja Google'i tulemusele, suutsin ma kirjutada töötava mooduli ja tunda end tuumahäkkerina, samas ka skripditiitina. Lisaks sellele külastasin Kernel Newbies kanalit, kus mulle soovitati kasutada schedule_work() täiendavalt kutsumiseks call_usermodehelper() otse ise konksu sees ja mind naljatati, õigustatult kahtlustades petuskeemi. Saja rea kood maksis mulle rohkem kui nädal vabade päevade arendust. Edukas kogemus, mis lõhkus minu isikliku müüdi süsteemse arenduse ülemäära keerukusest.
Kui keegi nõustub GitHubis koodi üle vaatama, oleksin tänulik. Olen peaaegu kindel, et tegin palju tobe vigu, eriti stringidega töötamisel.
Allikas: habr.com

