ICMP üzerinden nükleer kabuk

ICMP üzerinden nükleer kabuk

TL; DR: SSH'niz çökse bile ICMP yükünden gelen komutları okuyacak ve bunları sunucuda çalıştıracak bir çekirdek modülü yazıyorum. En sabırsızlar için tüm kod github.

Dikkat! Deneyimli C programcıları kan gözyaşlarına boğulma tehlikesiyle karşı karşıyadır! Terminolojide yanılıyor bile olabilirim ama her türlü eleştiriye açığım. Bu yazı, C programlama hakkında çok kaba bir fikri olan ve Linux'un iç kısımlarına bakmak isteyenler için hazırlanmıştır.

İlk mesajıma yapılan yorumlarda Makale Bazı “normal” protokolleri, özellikle HTTPS, ICMP ve hatta DNS'yi taklit edebilen SoftEther VPN'den bahsetti. HTTP(S)'ye çok aşina olduğumdan ve ICMP ve DNS üzerinden tünel oluşturmayı öğrenmem gerektiğinden, bunlardan ilkinin nasıl çalışacağını ancak hayal edebiliyorum.

ICMP üzerinden nükleer kabuk

Evet, 2020'de ICMP paketlerine isteğe bağlı bir veri yükü ekleyebileceğinizi öğrendim. Ama geç olması hiç olmamasından iyidir! Ve bu konuda bir şeyler yapılabileceğine göre, yapılması gerekiyor. Günlük hayatımda SSH de dahil olmak üzere çoğunlukla komut satırını kullandığım için aklıma ilk olarak ICMP kabuğu fikri geldi. Ve tam bir bullshield bingo'su oluşturmak için, bunu sadece kabaca bir fikrimin olduğu bir dilde bir Linux modülü olarak yazmaya karar verdim. Böyle bir kabuk işlemler listesinde görünmeyecek, çekirdeğe yükleyebilirsiniz ve dosya sisteminde olmayacak, dinleme bağlantı noktaları listesinde şüpheli bir şey görmeyeceksiniz. Yetenekleri açısından, bu tam teşekküllü bir rootkit, ancak onu geliştirmeyi ve Yük Ortalaması SSH aracılığıyla oturum açmak ve en azından yürütmek için çok yüksek olduğunda son çare olarak kullanmayı umuyorum. echo i > /proc/sysrq-triggerYeniden başlatmadan erişimi geri yüklemek için.

Bir metin editörü, Python ve C, Google'da temel programlama becerileri alıyoruz ve sanal Her şey bozulursa bıçağın altına koymaktan çekinmezsiniz (isteğe bağlı - yerel VirtualBox/KVM/vb) ve hadi gidelim!

İstemci tarafı

Müşteri açısından yaklaşık 80 satırlık bir senaryo yazmam gerekecekmiş gibi geldi bana, ama bunu benim için yapan nazik insanlar da vardı. tüm iş. Kodun beklenmedik derecede basit olduğu ve 10 önemli satıra sığdığı ortaya çıktı:

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()

Betik iki argüman alır; bir adres ve bir yük. Göndermeden önce yükün önüne bir anahtar gelir run:rasgele yüklere sahip paketleri hariç tutmak için buna ihtiyacımız olacak.

Çekirdek, paketleri oluşturmak için ayrıcalıklara ihtiyaç duyar, bu nedenle betiğin süper kullanıcı olarak çalıştırılması gerekecektir. Yürütme izinlerini vermeyi ve scapy'nin kendisini kurmayı unutmayın. Debian'ın adında bir paketi var python3-scapy. Artık her şeyin nasıl çalıştığını kontrol edebilirsiniz.

Komutun çalıştırılması ve çıktısının alınması
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'da böyle görünüyor
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

Yanıt paketindeki yük değişmez.

Çekirdek modülü

Bir Debian sanal makinesi oluşturmak için en azından ihtiyacınız olacak make и linux-headers-amd64geri kalanı bağımlılıklar şeklinde gelecektir. Makalede kodun tamamını vermeyeceğim; Github'a kopyalayabilirsiniz.

Kanca kurulumu

Başlangıç ​​olarak modülü yüklemek ve boşaltmak için iki fonksiyona ihtiyacımız var. Boşaltma işlevi gerekli değildir, ancak daha sonra rmmod çalışmayacaktır; modül yalnızca kapatıldığında boşaltılacaktır.

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

Burada neler oluyor:

  1. Modülün kendisini ve ağ filtresini değiştirmek için iki başlık dosyası alınır.
  2. Tüm işlemler bir netfiltreden geçer, içine kancalar yerleştirebilirsiniz. Bunu yapmak için kancanın yapılandırılacağı yapıyı bildirmeniz gerekir. En önemli şey kanca olarak yürütülecek işlevi belirtmektir: nfho.hook = icmp_cmd_executor; Fonksiyonun kendisine daha sonra değineceğim.
    Daha sonra paketin işlem süresini ayarlıyorum: NF_INET_PRE_ROUTING paketin çekirdekte ilk göründüğünde işlenmesini belirtir. Kullanılabilir NF_INET_POST_ROUTING Paketi çekirdekten çıkarken işlemek için.
    Filtreyi IPv4 olarak ayarladım: nfho.pf = PF_INET;.
    Kancama en yüksek önceliği veriyorum: nfho.priority = NF_IP_PRI_FIRST;
    Ve veri yapısını gerçek kanca olarak kaydediyorum: nf_register_net_hook(&init_net, &nfho);
  3. Son işlev kancayı kaldırır.
  4. Derleyicinin şikayet etmemesi için lisans açıkça belirtilir.
  5. fonksiyonlar module_init() и module_exit() Modülü başlatmak ve sonlandırmak için diğer işlevleri ayarlayın.

Yükün alınması

Şimdi yükü çıkarmamız gerekiyor, bunun en zor iş olduğu ortaya çıktı. Çekirdek, yüklerle çalışmak için yerleşik işlevlere sahip değildir; yalnızca üst düzey protokollerin başlıklarını ayrıştırabilirsiniz.

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

Ne olur:

  1. Bu sefer IP ve ICMP başlıklarını değiştirmek için ek başlık dosyaları eklemek zorunda kaldım.
  2. Maksimum hat uzunluğunu ayarladım: #define MAX_CMD_LEN 1976. Neden tam olarak bu? Çünkü derleyici bundan şikayetçi! Zaten bana yığını ve yığını anlamam gerektiğini söylediler, bir gün bunu kesinlikle yapacağım ve hatta belki kodu düzelteceğim. Hemen komutu içerecek satırı ayarladım: char cmd_string[MAX_CMD_LEN];. Tüm işlevlerde görünür olması gerekir; bundan 9. paragrafta daha ayrıntılı olarak bahsedeceğim.
  3. Şimdi başlatmamız gerekiyor (struct work_struct my_work;) yapılandırın ve onu başka bir işleve bağlayın (DECLARE_WORK(my_work, work_handler);). Bunun neden gerekli olduğundan da dokuzuncu paragrafta bahsedeceğim.
  4. Şimdi kanca olacak bir fonksiyon ilan ediyorum. Tür ve kabul edilen argümanlar netfilter tarafından belirlenir, biz sadece bunlarla ilgileniyoruz skb. Bu, bir paket hakkında mevcut tüm bilgileri içeren temel bir veri yapısı olan bir soket arabelleğidir.
  5. Fonksiyonun çalışması için iki yapıya ve iki yineleyici de dahil olmak üzere birçok değişkene ihtiyacınız olacak.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Mantıkla başlayabiliriz. Modülün çalışması için ICMP Echo dışında hiçbir pakete ihtiyaç yoktur, bu nedenle yerleşik işlevleri kullanarak arabelleği ayrıştırır ve ICMP olmayan ve Echo olmayan tüm paketleri atarız. Geri dönmek NF_ACCEPT paketin kabul edildiği anlamına gelir, ancak paketleri iade ederek de bırakabilirsiniz. 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ıklarını kontrol etmeden ne olacağını test etmedim. Asgari C bilgim bana, ek kontroller yapılmazsa korkunç bir şeyin kaçınılmaz olduğunu söylüyor. Beni bundan caydırırsanız çok sevinirim!

  7. Artık paket tam ihtiyacınız olan türde olduğuna göre verileri çıkarabilirsiniz. Yerleşik bir işlev olmadan, öncelikle yükün başlangıcını gösteren bir işaretçi almanız gerekir. Bu tek bir yerde yapılır, işaretçiyi ICMP başlığının başlangıcına götürmeniz ve bu başlığın boyutuna taşımanız gerekir. Her şey yapıyı kullanır icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Başlığın sonu, içindeki yükün sonuyla eşleşmelidir. skb, bu nedenle onu ilgili yapıdan nükleer araçlar kullanarak elde ederiz: tail = skb_tail_pointer(skb);.

    ICMP üzerinden nükleer kabuk

    Resim çalındı bundan dolayı, soket arabelleği hakkında daha fazla bilgi edinebilirsiniz.

  8. Başlangıç ​​ve bitiş işaretçilerine sahip olduğunuzda verileri bir dizeye kopyalayabilirsiniz. cmd_string, bir önekin olup olmadığını kontrol edin run: ve eksikse paketi atın veya bu öneki kaldırarak satırı yeniden yazın.
  9. İşte bu, artık başka bir işleyiciyi arayabilirsiniz: schedule_work(&my_work);. Böyle bir çağrıya parametre aktarmak mümkün olmadığından komutun bulunduğu satırın global olması gerekir. schedule_work() aktarılan yapıyla ilişkili işlevi görev zamanlayıcının genel kuyruğuna yerleştirecek ve tamamlayacak, böylece komutun tamamlanmasını beklemenize gerek kalmayacaktır. Bu gereklidir çünkü kancanın çok hızlı olması gerekir. Aksi halde seçiminiz hiçbir şeyin başlamaması veya çekirdek paniği yaşamanızdır. Gecikme ölüm gibidir!
  10. İşte bu, paketi karşılık gelen bir iadeyle kabul edebilirsiniz.

Kullanıcı alanında bir programın çağrılması

Bu işlev en anlaşılır olanıdır. Adı verildi DECLARE_WORK(), türü ve kabul edilen argümanlar ilginç değil. Komutun olduğu satırı alıp tamamen kabuğa aktarıyoruz. Bırakın ayrıştırma, ikili dosyaları arama ve diğer her şeyle ilgilensin.

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. Bağımsız değişkenleri bir dize dizisine ayarlayın argv[]. Herkesin, programların boşluklarla dolu sürekli bir çizgi olarak değil, aslında bu şekilde yürütüldüğünü bildiğini varsayacağım.
  2. Ortam değişkenlerini ayarlayın. Hepsinin zaten birleştirilmiş olduğunu umarak yalnızca minimum yol kümesiyle PATH ekledim /bin с /usr/bin и /sbin с /usr/sbin. Diğer yollar pratikte nadiren önem taşır.
  3. Bitti, hadi yapalım! Çekirdek işlevi call_usermodehelper() girişi kabul eder. ikili dosyanın yolu, argüman dizisi, ortam değişkenleri dizisi. Burada ayrıca herkesin yürütülebilir dosyaya giden yolu ayrı bir argüman olarak aktarmanın anlamını anladığını varsayıyorum, ancak sorabilirsiniz. Son argüman sürecin tamamlanmasının beklenip beklenmeyeceğini belirtir (UMH_WAIT_PROC), süreç başlangıcı (UMH_WAIT_EXEC) veya hiç beklemeyin (UMH_NO_WAIT). Biraz daha var mı UMH_KILLABLE, içine bakmadım.

montaj

Çekirdek modüllerinin montajı, çekirdek yapım çerçevesi aracılığıyla gerçekleştirilir. İsminde make çekirdek sürümüne bağlı özel bir dizinin içinde (burada tanımlanır: KERNELDIR:=/lib/modules/$(shell uname -r)/build) ve modülün konumu değişkene iletilir M argümanlarda. icmpshell.ko ve temiz hedefler bu çerçeveyi tamamen kullanır. İÇİNDE obj-m modüle dönüştürülecek nesne dosyasını belirtir. Yeniden yapılan sözdizimi main.o в icmpshell.o (icmpshell-objs = main.o) bana pek mantıklı gelmiyor ama öyle 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 topluyoruz: make. Yükleniyor: insmod icmpshell.ko. Bitti, şunları kontrol edebilirsiniz: sudo ./send.py 45.11.26.232 "date > /tmp/test". Makinenizde bir dosya varsa /tmp/test ve isteğin gönderildiği tarihi içerir, bu da sizin her şeyi doğru yaptığınız, benim de her şeyi doğru yaptığım anlamına gelir.

Sonuç

Nükleer gelişimle ilgili ilk deneyimim beklediğimden çok daha kolaydı. C dilinde geliştirme deneyimim olmamasına, derleyici ipuçlarına ve Google sonuçlarına odaklanmama rağmen, çalışan bir modül yazabildim ve kendimi bir çekirdek korsanı ve aynı zamanda bir senaryo çocuğu gibi hissettim. Ayrıca Kernel Newbies kanalına da gittim ve orada kullanmam söylendi. schedule_work() aramak yerine call_usermodehelper() kancanın içinde ve haklı olarak bir dolandırıcılıktan şüphelenerek onu utandırdı. Yüz satır kod, boş zamanlarımda yaklaşık bir haftalık geliştirmeme mal oldu. Sistem geliştirmenin aşırı karmaşıklığı hakkındaki kişisel efsanemi yok eden başarılı bir deneyim.

Birisi Github'da kod incelemesi yapmayı kabul ederse minnettar olacağım. Özellikle tellerle çalışırken pek çok aptalca hata yaptığımdan eminim.

ICMP üzerinden nükleer kabuk

Kaynak: habr.com

Yorum ekle