Միջուկային արկը ICMP-ի վրայով

Միջուկային արկը ICMP-ի վրայով

TL. DRԵս գրում եմ միջուկի մոդուլ, որը կկարդա հրամանները ICMP ծանրաբեռնվածությունից և կկատարի դրանք սերվերում, նույնիսկ եթե ձեր SSH-ը խափանվի: Ամենաանհամբերների համար բոլոր ծածկագրերն են GitHub.

Զգուշացեք Փորձառու C ծրագրավորողները ռիսկի են դիմում արյան արցունքներ պայթել: Գուցե նույնիսկ տերմինաբանության մեջ սխալվում եմ, բայց ցանկացած քննադատություն ողջունելի է։ Գրառումը նախատեսված է նրանց համար, ովքեր շատ կոպիտ պատկերացում ունեն C ծրագրավորման մասին և ցանկանում են ուսումնասիրել Linux-ի ներքինը:

Իմ առաջինի մեկնաբանություններում Հոդված նշեց SoftEther VPN-ը, որը կարող է ընդօրինակել որոշ «սովորական» արձանագրություններ, մասնավորապես՝ HTTPS, ICMP և նույնիսկ DNS: Ես պատկերացնում եմ, որ դրանցից միայն առաջինն է աշխատում, քանի որ ես շատ լավ ծանոթ եմ HTTP(S-ին), և ես պետք է սովորեի թունելավորում ICMP-ի և DNS-ի միջոցով:

Միջուկային արկը ICMP-ի վրայով

Այո, 2020 թվականին ես իմացա, որ դուք կարող եք կամայական ծանրաբեռնվածություն տեղադրել ICMP փաթեթների մեջ: Բայց լավ է ուշ, քան երբեք: Եվ քանի որ դրա դեմ ինչ-որ բան կարելի է անել, ուրեմն պետք է դա անել: Քանի որ իմ առօրյա կյանքում ես ամենից հաճախ օգտագործում եմ հրամանի տողը, ներառյալ SSH-ի միջոցով, առաջինը մտքովս եկավ ICMP կեղևի գաղափարը: Եվ ամբողջական բուլշիլդ բինգո հավաքելու համար ես որոշեցի այն գրել որպես Linux մոդուլ մի լեզվով, որի մասին ես միայն մոտավոր պատկերացում ունեմ: Նման կեղևը չի երևա պրոցեսների ցանկում, կարող եք բեռնել այն միջուկում և այն չի լինի ֆայլային համակարգում, դուք չեք տեսնի որևէ կասկածելի բան լսելու պորտերի ցանկում։ Իր հնարավորությունների առումով սա լիարժեք rootkit է, բայց ես հուսով եմ, որ այն կբարելավեմ և կօգտագործեմ որպես վերջին միջոց, երբ բեռնվածության միջինը չափազանց բարձր է 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!

Ահա թե ինչ տեսք ունի այն 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)

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. Ինչո՞ւ հենց սա: Որովհետև կոմպիլյատորը բողոքում է դրանից։ Նրանք ինձ արդեն առաջարկել են, որ ես պետք է հասկանամ stack-ը և heap-ը, մի օր ես անպայման դա կանեմ և գուցե նույնիսկ կուղղեմ կոդը: Ես անմիջապես սահմանեցի այն տողը, որը կպարունակի հրամանը. char cmd_string[MAX_CMD_LEN];. Այն պետք է տեսանելի լինի բոլոր գործառույթներում, ես այս մասին ավելի մանրամասն կխոսեմ 9-րդ պարբերությունում:
  3. Այժմ մենք պետք է նախաստորագրենք (struct work_struct my_work;) կառուցվածքը և միացնել այն մեկ այլ ֆունկցիայի հետ (DECLARE_WORK(my_work, work_handler);) Թե ինչու է դա անհրաժեշտ, ես կխոսեմ նաև իններորդ պարբերության մեջ։
  4. Հիմա ես մի ֆունկցիա եմ հայտարարում, որը կլինի կեռիկ։ Տեսակը և ընդունված փաստարկները թելադրում է netfilter-ը, մեզ միայն հետաքրքրում է 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 և ոչ Echo փաթեթները: Վերադարձ 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 և այն պարունակում է հարցումն ուղարկելու ամսաթիվը, ինչը նշանակում է, որ դուք ամեն ինչ ճիշտ եք արել, իսկ ես՝ ճիշտ:

Ամփոփում

Միջուկային զարգացման իմ առաջին փորձը շատ ավելի հեշտ էր, քան ես սպասում էի: Նույնիսկ առանց C-ում զարգանալու փորձի, կենտրոնանալով կոմպիլյատորների ակնարկների և Google-ի արդյունքների վրա, ես կարողացա գրել աշխատանքային մոդուլ և ինձ միջուկի հաքեր զգալ, և միևնույն ժամանակ սցենարի երեխա: Բացի այդ, ես գնացի Kernel Newbies ալիք, որտեղ ինձ ասացին օգտագործել schedule_work() զանգելու փոխարեն call_usermodehelper() ներսում կեռիկը և ամաչեց նրան՝ իրավացիորեն կասկածելով խաբեության մասին: Հարյուր տող կոդն ինձ մոտ մեկ շաբաթ արժեցել է ազատ ժամանակի մշակում: Հաջողակ փորձ, որը ոչնչացրեց իմ անձնական առասպելը համակարգի զարգացման ճնշող բարդության մասին:

Եթե ​​ինչ-որ մեկը համաձայնի Github-ում կոդի վերանայում անել, ես շնորհակալ կլինեմ: Համոզված եմ, որ շատ հիմար սխալներ եմ թույլ տվել, հատկապես լարերի հետ աշխատելիս:

Միջուկային արկը ICMP-ի վրայով

Source: www.habr.com

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