ICMP үстүндөгү өзөктүк кабык

ICMP үстүндөгү өзөктүк кабык

TL; DR: Мен өзөк модулун жазып жатам, ал ICMP пайдалуу жүгүнөн буйруктарды окуп, SSH бузулуп калса да аларды серверде аткарат. Эң чыдабагандар үчүн бардык код Github.

Абайлагыла! Тажрыйбалуу C программисттери көз жашын төгүп коюшу мүмкүн! Мен терминологияда жаңылышым мүмкүн, бирок ар кандай сын-пикир кабыл алынат. Пост C программалоосу жөнүндө өтө орой түшүнүгү бар жана Linuxтун ички сырларын карагысы келгендер үчүн арналган.

Менин биринчи комментарийге макала кээ бир "кадимки" протоколдорду, атап айтканда HTTPS, ICMP жана атүгүл DNSти туурай турган SoftEther VPN жөнүндө сөз болду. Мен алардын биринчиси гана иштеп жатканын элестете алам, анткени мен HTTP(S) менен жакшы таанышмын жана мен ICMP жана DNS аркылуу туннелди үйрөнүшүм керек болчу.

ICMP үстүндөгү өзөктүк кабык

Ооба, 2020-жылы мен ICMP пакеттерине ыктыярдуу пайдалуу жүктү кыстара аларыңызды билдим. Бирок эч качан караганда жакшы кеч! Ал эми бул жөнүндө бир нерсе кылуу мүмкүн болгондуктан, аны жасоо керек. Күнүмдүк жашоомдо мен көбүнчө буйрук сабын, анын ичинде SSH аркылуу колдонгондуктан, ICMP кабыгынын идеясы биринчи жолу оюма келди. Жана толук буллшилд бингону чогултуу үчүн, мен аны 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 Эходон башка пакеттердин кереги жок, ошондуктан биз буферди орнотулган функцияларды колдонуп анализдейбиз жана бардык 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 аталыштарын текшербей туруп эмне болорун сынаган жокмун. Менин С боюнча минималдуу билимим кошумча текшерүүлөрсүз коркунучтуу нерсе болушу мүмкүн экенин айтат. Мени бул нерседен тайдырсаңыз, мен кубанычтамын!

  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 ядро версиясына байланган атайын каталогдун ичинде (бул жерде аныкталган: 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 үстүндөгү өзөктүк кабык

Source: www.habr.com

Комментарий кошуу