Нуклеарна граната над ICMP

Нуклеарна граната над ICMP

TL; ДР: Пишувам модул на кернелот што ќе чита команди од ICMP товарот и ќе ги извршува на серверот дури и ако вашиот SSH се сруши. За најнетрпеливите, целата шифра е github.

Внимание! Искусните C програмери ризикуваат да пукнат во солзи од крв! Можеби и грешам во терминологијата, но секоја критика е добредојдена. Објавата е наменета за оние кои имаат многу груба идеја за програмирање C и сакаат да ја разгледаат внатрешноста на Linux.

Во коментарите на мојот прв Член спомна SoftEther VPN, кој може да имитира некои „редовни“ протоколи, особено HTTPS, ICMP, па дури и DNS. Можам да замислам само првиот од нив да работи, бидејќи сум многу запознаен со HTTP(S) и морав да научам тунелирање преку ICMP и DNS.

Нуклеарна граната над ICMP

Да, во 2020 година дознав дека можете да вметнете произволна носивост во ICMP пакетите. Но, подобро доцна отколку никогаш! И бидејќи нешто може да се направи во врска со тоа, тогаш тоа треба да се направи. Бидејќи во мојот секојдневен живот најчесто ја користам командната линија, вклучително и преку SSH, идејата за школка ICMP најпрво ми дојде на ум. И со цел да составам комплетно бинго, решив да го напишам како модул на Линукс на јазик за кој имам само груба идеја. Таквата школка нема да биде видлива во списокот на процеси, можете да ја вчитате во кернелот и нема да биде на датотечниот систем, нема да видите ништо сомнително во списокот со порти за слушање. Во однос на неговите можности, ова е полноправно руткит, но се надевам дека ќе го подобрам и ќе го користам како краен случај кога Просекот на оптоварување е превисок за да се најавите преку 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. Дебиан има пакет наречен 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 и не-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 во посебен директориум врзан за верзијата на јадрото (дефиниран овде: KERNELDIR:=/lib/modules/$(shell uname -r)/build), а локацијата на модулот се пренесува на променливата M во аргументите. icmpshell.ko и чистите цели целосно ја користат оваа рамка. ВО 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

Извор: www.habr.com

Додадете коментар