ICMP үстіндегі ядролық қабық

ICMP үстіндегі ядролық қабық

TL; DR: Мен ICMP пайдалы жүктемесінен пәрмендерді оқитын және SSH бұзылса да серверде орындайтын ядро ​​модулін жазып жатырмын. Ең шыдамсыз үшін барлық код GitHub.

Назар аударыңыз! Тәжірибелі C программистері қанның жасы ағып кету қаупі бар! Мен тіпті терминологияда қателесетін шығармын, бірақ кез келген сын құпталады. Пост C бағдарламалау туралы өте дөрекі идеясы бар және Linux-тың ішкі жағын қарастырғысы келетіндерге арналған.

Менің бірінші пікірімдегі түсініктемелерде мақала кейбір «тұрақты» протоколдарға, атап айтқанда HTTPS, ICMP және тіпті DNS-ге еліктейтін SoftEther VPN-ді атады. Мен олардың біріншісі ғана жұмыс істейтінін елестете аламын, өйткені мен HTTP(S)-мен өте жақсы таныспын және ICMP және DNS арқылы туннель жасауды үйренуім керек болды.

ICMP үстіндегі ядролық қабық

Иә, 2020 жылы мен ICMP пакеттеріне ерікті пайдалы жүктемені енгізуге болатынын білдім. Бірақ ешқашандан кеш жақсы! Бұл туралы бірдеңе жасауға болатындықтан, оны жасау керек. Күнделікті өмірімде пәрмен жолын, соның ішінде SSH арқылы жиі қолданатындықтан, ICMP қабықшасы идеясы бірінші болып ойыма келді. Толық bullshield бинго құрастыру үшін мен оны Linux модулі ретінде менде шамалы ғана түсінетін тілде жазуды шештім. Мұндай қабық процестер тізімінде көрінбейді, сіз оны ядроға жүктей аласыз және ол файлдық жүйеде болмайды, тыңдау порттарының тізімінде күдікті ештеңе көрмейсіз. Оның мүмкіндіктері бойынша бұл толыққанды руткит, бірақ мен оны жақсартамын деп үміттенемін және оны SSH арқылы кіру және кем дегенде орындау үшін Жүктеменің орташа мәні тым жоғары болған кезде соңғы құрал ретінде пайдаланамын деп үміттенемін. echo i > /proc/sysrq-triggerқайта жүктеусіз қол жеткізуді қалпына келтіру үшін.

Біз мәтіндік редакторды, Python және C, Google және негізгі бағдарламалау дағдыларын аламыз виртуалды Егер бәрі бұзылса (міндетті емес - жергілікті VirtualBox/KVM/т.б.) пышақ астына қоюға қарсы емессіз және кеттік!

Клиент бөлігі

Маған клиент бөлігі үшін шамамен 80 жолдан тұратын сценарий жазу керек сияқты көрінді, бірақ мен үшін оны жасаған мейірімді адамдар болды. барлық жұмыс. Код 10 маңызды жолға сәйкес келетін күтпеген жерден қарапайым болып шықты:

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

Сценарий екі аргументті, мекенжайды және пайдалы жүктемені алады. Жіберу алдында пайдалы жүктің алдында кілт қойылады run:, ол кездейсоқ пайдалы жүктемелері бар пакеттерді алып тастау үшін қажет болады.

Ядро пакеттерді жасау үшін артықшылықтарды талап етеді, сондықтан сценарий суперпайдаланушы ретінде іске қосылуы керек. Орындауға рұқсат беруді және scapy-ді орнатуды ұмытпаңыз. Debian деп аталатын пакеті бар python3-scapy. Енді сіз мұның бәрі қалай жұмыс істейтінін тексере аласыз.

Команданы орындау және шығару
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!

Бұл иісшіге ұқсайды
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

Жауап бумасындағы пайдалы жүктеме өзгермейді.

Ядро модулі

Debian виртуалды машинасын құру үшін сізге кем дегенде қажет make и linux-headers-amd64, қалғандары тәуелділіктер түрінде келеді. Мен мақалада барлық кодты бермеймін; оны Github-та клондауға болады.

Ілмекті орнату

Бастау үшін модульді жүктеу және оны босату үшін бізге екі функция қажет. Түсіру функциясы қажет емес, бірақ содан кейін 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);

Бұл жерде не болып жатыр:

  1. Модульдің өзін және желі сүзгісін басқару үшін екі тақырып файлы тартылады.
  2. Барлық операциялар желілік сүзгіден өтеді, оған ілмектерді орнатуға болады. Ол үшін ілмек конфигурацияланатын құрылымды жариялау керек. Ең бастысы - ілмек ретінде орындалатын функцияны көрсету: nfho.hook = icmp_cmd_executor; Мен функцияның өзіне кейінірек жетемін.
    Содан кейін пакеттің өңдеу уақытын орнаттым: NF_INET_PRE_ROUTING буманы ядрода бірінші рет пайда болған кезде өңдеуді көрсетеді. Қолдануға болады NF_INET_POST_ROUTING ядродан шыққан кезде пакетті өңдеу үшін.
    Мен сүзгіні IPv4 етіп орнаттым: nfho.pf = PF_INET;.
    Мен ілгегіме ең жоғары басымдық беремін: nfho.priority = NF_IP_PRI_FIRST;
    Мен деректер құрылымын нақты ілмек ретінде тіркеймін: nf_register_net_hook(&init_net, &nfho);
  3. Соңғы функция ілгекті алып тастайды.
  4. Компилятор шағымданбауы үшін лицензия анық көрсетілген.
  5. Функциялар module_init() и module_exit() модульді инициализациялау және тоқтату үшін басқа функцияларды орнатыңыз.

Пайдалы жүктемені алу

Енді біз пайдалы жүкті алуымыз керек, бұл ең қиын тапсырма болып шықты. Ядрода пайдалы жүктемелермен жұмыс істеуге арналған кірістірілген функциялар жоқ, тек жоғары деңгейлі хаттамалардың тақырыптарын талдауға болады.

#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;
}

Не болып жатыр:

  1. Бұл жолы IP және ICMP тақырыптарын өңдеу үшін қосымша тақырып файлдарын қосуға тура келді.
  2. Мен жолдың максималды ұзындығын орнаттым: #define MAX_CMD_LEN 1976. Неліктен дәл бұл? Өйткені компилятор бұл туралы шағымданады! Олар маған стек пен үйінді түсінуім керек екенін айтты, бір күні мен мұны міндетті түрде жасаймын және тіпті кодты түзетемін. Мен бірден пәрменді қамтитын жолды орнатамын: char cmd_string[MAX_CMD_LEN];. Ол барлық функцияларда көрінуі керек, мен бұл туралы толығырақ 9-тармақта айтатын боламын.
  3. Енді біз инициализациялауымыз керек (struct work_struct my_work;) құрылымы және оны басқа функциямен байланыстыру (DECLARE_WORK(my_work, work_handler);). Бұл не үшін қажет екендігі туралы мен тоғызыншы абзацта айтайын.
  4. Енді мен ілмек болатын функцияны жариялаймын. Түрі мен қабылданған аргументтерді желі сүзгісі белгілейді, бізді тек қызықтырады skb. Бұл розетка буфері, пакет туралы барлық қолжетімді ақпаратты қамтитын негізгі деректер құрылымы.
  5. Функция жұмыс істеуі үшін сізге екі құрылым және бірнеше айнымалылар, соның ішінде екі иератор қажет болады.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Біз логикадан бастай аламыз. Модуль жұмыс істеуі үшін ICMP Echo-дан басқа пакеттер қажет емес, сондықтан біз кірістірілген функцияларды пайдаланып буферді талдаймыз және барлық ICMP емес және жаңғырық емес пакеттерді лақтырамыз. Қайту NF_ACCEPT пакетті қабылдауды білдіреді, бірақ сіз қайтару арқылы пакеттерді тастай аласыз 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;
      }

    Мен IP тақырыптарын тексермей не болатынын тексерген жоқпын. Менің C туралы ең аз білімім қосымша тексерулерсіз қорқынышты нәрсе болуы мүмкін екенін айтады. Мені осыдан тайдырсаңыз, қуанамын!

  7. Енді пакет сізге қажет нақты түрге ие болғандықтан, деректерді шығарып алуға болады. Кірістірілген функция болмаса, алдымен пайдалы жүктеменің басына көрсеткішті алу керек. Бұл бір жерде орындалады, көрсеткішті ICMP тақырыбының басына апарып, оны осы тақырыптың өлшеміне жылжыту керек. Барлығы құрылымды пайдаланады icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Тақырыптың соңы пайдалы жүктеменің соңына сәйкес келуі керек skb, сондықтан біз оны сәйкес құрылымнан ядролық құралдарды пайдалана отырып аламыз: tail = skb_tail_pointer(skb);.

    ICMP үстіндегі ядролық қабық

    Сурет ұрланған мұнда, розетка буфері туралы көбірек оқуға болады.

  8. Басы мен аяғына көрсеткіштер болғаннан кейін деректерді жолға көшіруге болады cmd_string, префикстің бар-жоғын тексеріңіз run: және, егер ол жоқ болса, пакетті тастаңыз немесе осы префиксті алып тастап, жолды қайта жазыңыз.
  9. Міне, енді сіз басқа өңдеушіге қоңырау шала аласыз: schedule_work(&my_work);. Мұндай қоңырауға параметрді беру мүмкін болмайтындықтан, пәрмені бар жол ғаламдық болуы керек. schedule_work() тапсырылған құрылыммен байланысты функцияны тапсырма жоспарлаушының жалпы кезегіне орналастырады және пәрменнің аяқталуын күтпеуге мүмкіндік береді. Бұл қажет, себебі ілмек өте жылдам болуы керек. Әйтпесе, сіздің таңдауыңыз - ештеңе басталмайды немесе сіз ядролық дүрбелең аласыз. Кешігу өліммен тең!
  10. Міне, сіз пакетті сәйкес қайтарыммен қабылдай аласыз.

Пайдаланушы кеңістігіндегі бағдарламаны шақыру

Бұл функция ең түсінікті. Оның аты берілді DECLARE_WORK(), түрі мен қабылданған дәлелдер қызықты емес. Біз командамен сызықты алып, оны толығымен қабықшаға береміз. Ол талдаумен, екілік файлдарды іздеумен және басқалармен айналыссын.

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. Аргументтерді жолдар массивіне орнатыңыз argv[]. Бағдарламалар бос орындармен үздіксіз сызық ретінде емес, шын мәнінде осылай орындалатынын бәрі біледі деп ойлаймын.
  2. Ортаның айнымалы мәндерін орнатыңыз. Мен олардың барлығы біріктірілген деп үміттеніп, ең аз жолдар жиынтығымен ғана PATH енгіздім /bin с /usr/bin и /sbin с /usr/sbin. Басқа жолдар практикада сирек маңызды.
  3. Дайын, жасайық! Ядро функциясы call_usermodehelper() енгізуді қабылдайды. екілік жүйеге жол, аргументтер массиві, орта айнымалыларының массиві. Мұнда мен әркім орындалатын файлға жолды бөлек дәлел ретінде берудің мағынасын түсінеді деп ойлаймын, бірақ сіз сұрай аласыз. Соңғы аргумент процестің аяқталуын күту керектігін анықтайды (UMH_WAIT_PROC), процесті бастау (UMH_WAIT_EXEC) немесе мүлде күтпеу (UMH_NO_WAIT). Тағы бар UMH_KILLABLE, Мен оған қарамадым.

Ассамблея

Ядро модульдерін құрастыру ядроның make-framework арқылы орындалады. Қоңырау шалды make ядро нұсқасына байланыстырылған арнайы каталог ішінде (мұнда анықталған: KERNELDIR:=/lib/modules/$(shell uname -r)/build), ал модульдің орны айнымалыға беріледі M аргументтерде. icmpshell.ko және таза мақсаттар осы құрылымды толығымен пайдаланады. IN obj-m модульге түрлендірілетін нысан файлын көрсетеді. Қайта жасайтын синтаксис main.o в icmpshell.o (icmpshell-objs = main.o) маған өте қисынды болып көрінбейді, бірақ солай болсын.

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

Біз жинаймыз: make. Жүктелуде: insmod icmpshell.ko. Дайын, тексеруге болады: sudo ./send.py 45.11.26.232 "date > /tmp/test". Құрылғыңызда файл болса /tmp/test және онда сұрау жіберілген күн бар, яғни сіз бәрін дұрыс жасадыңыз және мен бәрін дұрыс жасадым.

қорытынды

Ядролық дамудағы алғашқы тәжірибем мен күткеннен әлдеқайда оңай болды. Компилятордың кеңестері мен Google нәтижелеріне назар аудара отырып, C тілінде даму тәжірибесі болмаса да, мен жұмыс модулін жазып, өзімді ядро ​​хакері және сонымен бірге сценарийші бала сияқты сезіне алдым. Сонымен қатар, мен Kernel Newbies арнасына бардым, онда маған пайдалану керектігін айтты schedule_work() қоңырау шалудың орнына call_usermodehelper() ілмектің өзінде және оны ұятқа қалдырды, алаяқтықтан дұрыс күдіктенді. Жүз жолдық код маған бос уақытымда бір аптаға жуық дамуға жұмсалды. Жүйені дамытудың күрделілігі туралы менің жеке мифімді жойған сәтті тәжірибе.

Егер біреу Github-та кодты тексеруге келіссе, мен ризамын. Мен көптеген ақымақ қателіктер жібергеніме сенімдімін, әсіресе жіптермен жұмыс істегенде.

ICMP үстіндегі ядролық қабық

Ақпарат көзі: www.habr.com

пікір қалдыру