ICMP üzərində nüvə qabığı

ICMP üzərində nüvə qabığı

TL; DR: Mən ICMP faydalı yükündən əmrləri oxuyacaq və SSH-niz qəzaya uğrasa belə onları serverdə yerinə yetirəcək bir nüvə modulu yazıram. Ən səbirsiz üçün bütün koddur GitHub.

Diqqət! Təcrübəli C proqramçıları qan gözyaşlarına boğulmaq təhlükəsi ilə üzləşirlər! Terminologiyada hətta səhv edə bilərəm, amma hər hansı bir tənqid xoşdur. Yazı C proqramlaşdırması haqqında çox kobud təsəvvürə malik olan və Linux-un içərisinə baxmaq istəyənlər üçün nəzərdə tutulub.

İlkimə şərhlərdə məqalə bəzi “müntəzəm” protokolları, xüsusən HTTPS, ICMP və hətta DNS-ni təqlid edə bilən SoftEther VPN-i qeyd etdi. Mən HTTP(S) ilə çox tanış olduğum üçün onlardan yalnız birincisinin işlədiyini təsəvvür edə bilərəm və ICMP və DNS üzərindən tunel qurmağı öyrənməli oldum.

ICMP üzərində nüvə qabığı

Bəli, 2020-ci ildə ICMP paketlərinə ixtiyari faydalı yük daxil edə biləcəyinizi öyrəndim. Ancaq gec heç vaxtdan yaxşıdır! Və bu barədə bir şey etmək mümkün olduğu üçün bunu etmək lazımdır. Gündəlik həyatımda ən çox əmr satırından, o cümlədən SSH vasitəsilə istifadə etdiyim üçün ağlıma ilk olaraq ICMP qabığı ideyası gəldi. Və tam bir bullshield bingo yığmaq üçün mən onu Linux modulu kimi yalnız təxmini fikrim olan bir dildə yazmağa qərar verdim. Belə bir qabıq proseslər siyahısında görünməyəcək, onu nüvəyə yükləyə bilərsiniz və o, fayl sistemində olmayacaq, dinləmə portları siyahısında şübhəli bir şey görməyəcəksiniz. İmkanları baxımından bu, tam hüquqlu bir rootkitdir, lakin ümid edirəm ki, onu təkmilləşdirəcəyəm və Yükləmə Ortalaması SSH vasitəsilə daxil olmaq və ən azı icra etmək üçün çox yüksək olduqda, onu son çarə kimi istifadə edəcəyəm. echo i > /proc/sysrq-triggeryenidən başlamadan girişi bərpa etmək.

Biz mətn redaktoru, Python və C, Google və əsas proqramlaşdırma bacarıqları alırıq virtual hər şey pozulduqda (isteğe bağlı - yerli VirtualBox/KVM/s.

Müştəri tərəfi

Mənə elə gəldi ki, müştəri hissəsi üçün təxminən 80 sətirdən ibarət ssenari yazmalıyam, amma bunu mənim üçün edən xeyirxah insanlar var idi. bütün iş. Kod gözlənilmədən sadə oldu, 10 əhəmiyyətli sətirə uyğun gəldi:

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 iki arqument, bir ünvan və bir yük götürür. Göndərməzdən əvvəl faydalı yükün qarşısında açar qoyulur run:, təsadüfi yükləri olan paketləri istisna etmək üçün bizə lazım olacaq.

Kernel paketləri hazırlamaq üçün imtiyazlar tələb edir, buna görə də skript super istifadəçi kimi işlədilməlidir. İcra icazələrini verməyi və scapy-nin özünü quraşdırmağı unutmayın. Debian adlı paket var python3-scapy. İndi hər şeyin necə işlədiyini yoxlaya bilərsiniz.

Komandanın işlədilməsi və çıxarılması
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!

Snayperdə belə görünür
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

Cavab paketindəki faydalı yük dəyişmir.

Kernel modulu

Debian virtual maşınında qurmaq üçün ən azı sizə lazım olacaq make и linux-headers-amd64, qalanları asılılıqlar şəklində gələcək. Məqalədə bütün kodu təqdim etməyəcəyəm, onu Github-da klonlaya bilərsiniz.

Qarmaq quraşdırma

Başlamaq üçün modulu yükləmək və boşaltmaq üçün bizə iki funksiya lazımdır. Boşaltma funksiyası tələb olunmur, lakin sonra rmmod işləməyəcək; modul yalnız söndürüldükdə boşaldılacaq.

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

Burda nə baş verir:

  1. Modulun özünü və şəbəkə filtrini idarə etmək üçün iki başlıq faylı çəkilir.
  2. Bütün əməliyyatlar şəbəkə filtrindən keçir, orada qarmaqlar qoya bilərsiniz. Bunu etmək üçün, çəngəl konfiqurasiya ediləcəyi strukturu elan etməlisiniz. Ən vacibi, çəngəl kimi yerinə yetiriləcək funksiyanı təyin etməkdir: nfho.hook = icmp_cmd_executor; Mən funksiyanın özünə sonra keçəcəyəm.
    Sonra paket üçün emal vaxtını təyin etdim: NF_INET_PRE_ROUTING paketin nüvədə ilk dəfə göründüyü zaman emal edilməsini təyin edir. İstifadə edilə bilər NF_INET_POST_ROUTING kerneldən çıxan kimi paketi emal etmək.
    Mən filtri IPv4-ə təyin etdim: nfho.pf = PF_INET;.
    Mən çəngəlmə ən yüksək üstünlük verirəm: nfho.priority = NF_IP_PRI_FIRST;
    Mən məlumat strukturunu faktiki çəngəl kimi qeyd edirəm: nf_register_net_hook(&init_net, &nfho);
  3. Son funksiya çəngəl çıxarır.
  4. Lisenziya aydın şəkildə göstərilib ki, kompilyator şikayət etməsin.
  5. Funksiyalar module_init() и module_exit() modulu işə salmaq və dayandırmaq üçün digər funksiyaları təyin edin.

Faydalı yükün alınması

İndi faydalı yükü çıxarmalıyıq, bu ən çətin iş oldu. Kerneldə faydalı yüklərlə işləmək üçün daxili funksiyalar yoxdur, siz yalnız daha yüksək səviyyəli protokolların başlıqlarını təhlil edə bilərsiniz.

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

Nə baş verir:

  1. Mən bu dəfə IP və ICMP başlıqlarını manipulyasiya etmək üçün əlavə başlıq faylları daxil etməli oldum.
  2. Maksimum xətt uzunluğunu təyin etdim: #define MAX_CMD_LEN 1976. Niyə məhz bu? Çünki kompilyator bundan şikayətlənir! Artıq mənə təklif etdilər ki, yığını və yığını başa düşməliyəm, nə vaxtsa bunu mütləq edəcəm və hətta kodu düzəldəcəm. Mən dərhal əmri ehtiva edən xətti təyin edirəm: char cmd_string[MAX_CMD_LEN];. Bütün funksiyalarda görünməlidir, bu barədə 9-cu bənddə daha ətraflı danışacağam.
  3. İndi işə salmalıyıq (struct work_struct my_work;) quruluşu və onu başqa bir funksiya ilə əlaqələndirin (DECLARE_WORK(my_work, work_handler);). Doqquzuncu abzasda bunun nə üçün lazım olduğunu da danışacağam.
  4. İndi mən bir çəngəl olacaq bir funksiyanı elan edirəm. Növ və qəbul edilən arqumentlər netfilter tərəfindən diktə edilir, bizi yalnız maraqlandırır skb. Bu, paket haqqında bütün mövcud məlumatları ehtiva edən əsas məlumat strukturu olan soket buferidir.
  5. Funksiyanın işləməsi üçün sizə iki struktur və bir neçə dəyişənə, o cümlədən iki iterator lazımdır.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Məntiqlə başlaya bilərik. Modulun işləməsi üçün ICMP Echo-dan başqa heç bir paketə ehtiyac yoxdur, ona görə də biz daxili funksiyalardan istifadə edərək buferi təhlil edirik və bütün qeyri-ICMP və Echo olmayan paketləri atırıq. Qayıt NF_ACCEPT paketin qəbulu deməkdir, lakin siz geri qaytarmaqla da paketləri buraxa bilərsiniz 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 başlıqlarını yoxlamadan nə olacağını sınamamışam. Mənim minimum C biliklərim mənə deyir ki, əlavə yoxlamalar olmadan dəhşətli bir şey baş verə bilər. Məni bundan çəkindirsəniz, şad olaram!

  7. İndi paket sizə lazım olan dəqiq tipdədir, siz məlumatları çıxara bilərsiniz. Daxili funksiya olmadan, ilk növbədə faydalı yükün başlanğıcına bir göstərici əldə etməlisiniz. Bu, bir yerdə edilir, göstəricini ICMP başlığının əvvəlinə aparmalı və onu bu başlığın ölçüsünə köçürməlisiniz. Hər şey strukturdan istifadə edir icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Başlığın ucu faydalı yükün sonuna uyğun olmalıdır skb, buna görə də onu müvafiq strukturdan nüvə vasitələrindən istifadə edərək əldə edirik: tail = skb_tail_pointer(skb);.

    ICMP üzərində nüvə qabığı

    Şəkil oğurlanıb buradan, siz rozetka buferi haqqında ətraflı oxuya bilərsiniz.

  8. Başlanğıc və son üçün göstəricilərə sahib olduqdan sonra məlumatları sətirə köçürə bilərsiniz cmd_string, prefiksin olub-olmadığını yoxlayın run: və ya paket yoxdursa, onu atın, ya da bu prefiksi silməklə sətri yenidən yazın.
  9. Budur, indi başqa bir idarəçiyə zəng edə bilərsiniz: schedule_work(&my_work);. Belə bir çağırışa parametr ötürmək mümkün olmadığı üçün əmri olan xətt qlobal olmalıdır. schedule_work() ötürülmüş strukturla əlaqəli funksiyanı tapşırıq planlaşdırıcısının ümumi növbəsinə yerləşdirəcək və əmrin tamamlanmasını gözləməməyə imkan verəcəkdir. Bu lazımdır, çünki çəngəl çox sürətli olmalıdır. Əks halda, seçiminiz heç bir şey başlamaması və ya kernel panikası almanızdır. Gecikmə ölüm kimidir!
  10. Budur, paketi müvafiq qaytarma ilə qəbul edə bilərsiniz.

İstifadəçi məkanında proqrama zəng etmək

Bu funksiya ən başa düşüləndir. Onun adı verilmişdir DECLARE_WORK(), növü və qəbul edilən arqumentlər maraqlı deyil. Komanda ilə xətti götürürük və tamamilə qabığa keçirik. Onun təhlili, ikili faylların axtarışı və başqa hər şeylə məşğul olmasına icazə verin.

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. Arqumentləri sətirlər massivinə təyin edin argv[]. Güman edirəm ki, hər kəs proqramların boşluqlarla davamlı bir xətt kimi deyil, əslində bu şəkildə icra edildiyini bilir.
  2. Ətraf mühit dəyişənlərini təyin edin. Onların hamısının artıq birləşdirildiyinə ümid edərək minimum yollar dəsti ilə yalnız PATH daxil etdim /bin с /usr/bin и /sbin с /usr/sbin. Digər yollar praktikada nadir hallarda əhəmiyyət kəsb edir.
  3. Bitdi, gəlin bunu edək! Kernel funksiyası call_usermodehelper() girişi qəbul edir. ikiliyə gedən yol, arqumentlər massivi, mühit dəyişənləri massivi. Burada mən də güman edirəm ki, hər kəs icra olunan fayla yolun ayrıca arqument kimi keçməsinin mənasını başa düşür, ancaq soruşa bilərsiniz. Son arqument prosesin tamamlanmasını gözləməyinizi müəyyən edir (UMH_WAIT_PROC), prosesin başlanğıcı (UMH_WAIT_EXEC) və ya heç gözləməyin (UMH_NO_WAIT). Daha varmı UMH_KILLABLE, mən buna baxmadım.

Məclis

Kernel modullarının yığılması kernel make-framework vasitəsilə həyata keçirilir. Zəng etdi make nüvə versiyası ilə əlaqəli xüsusi bir kataloq daxilində (burada müəyyən edilir: KERNELDIR:=/lib/modules/$(shell uname -r)/build) və modulun yeri dəyişənə ötürülür M arqumentlərdə. Icmpshell.ko və təmiz hədəflər bu çərçivədən tamamilə istifadə edir. IN obj-m modula çevriləcək obyekt faylını göstərir. Yenidən düzəldən sintaksis main.o в icmpshell.o (icmpshell-objs = main.o) mənə çox məntiqli görünmür, amma elə də olsun.

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

Biz toplayırıq: make. Yüklənir: insmod icmpshell.ko. Hazırdır, yoxlaya bilərsiniz: sudo ./send.py 45.11.26.232 "date > /tmp/test". Maşınınızda bir fayl varsa /tmp/test və sorğunun göndərilmə tarixini ehtiva edir, yəni siz hər şeyi düzgün etdiniz və mən hər şeyi düzgün etdim.

Nəticə

Nüvə inkişafı ilə bağlı ilk təcrübəm gözlədiyimdən daha asan oldu. Kompilyator göstərişlərinə və Google nəticələrinə diqqət yetirərək C-də inkişaf təcrübəsi olmasa belə, mən işləyən modul yaza bildim və özümü nüvə hakeri və eyni zamanda skript uşağı kimi hiss edə bildim. Bundan əlavə, Kernel Newbies kanalına getdim, orada istifadə etməyimi söylədim schedule_work() zəng etmək əvəzinə call_usermodehelper() çəngəlin içərisində və onu rüsvay etdi, haqlı olaraq bir fırıldaqdan şübhələndi. Boş vaxtlarımda yüz sətir kod mənə təxminən bir həftəlik inkişafa başa gəldi. Sistemin inkişafının hədsiz mürəkkəbliyi haqqında şəxsi mifimi məhv edən uğurlu təcrübə.

Kimsə Github-da kod araşdırması etməyə razı olarsa, minnətdar olaram. Əminəm ki, çoxlu axmaq səhvlərə yol vermişəm, xüsusən də simlərlə işləyərkən.

ICMP üzərində nüvə qabığı

Mənbə: www.habr.com

Добавить комментарий