Cangkang nuklir di atas ICMP

Cangkang nuklir di atas ICMP

TL; DR: Saya sedang menulis modul kernel yang akan membaca perintah dari muatan ICMP dan menjalankannya di server bahkan jika SSH Anda mogok. Bagi yang paling tidak sabar, semua kodenya ada github.

Perhatian! Pemrogram C berpengalaman berisiko menangis darah! Saya bahkan mungkin salah dalam terminologinya, tetapi kritik apa pun diterima. Postingan ini ditujukan bagi mereka yang memiliki gambaran kasar tentang pemrograman C dan ingin melihat bagian dalam Linux.

Di komentar pertama saya Artikel disebutkan SoftEther VPN, yang dapat meniru beberapa protokol β€œbiasa”, khususnya HTTPS, ICMP, dan bahkan DNS. Saya dapat membayangkan hanya yang pertama yang berfungsi, karena saya sangat familiar dengan HTTP(S), dan saya harus belajar tunneling melalui ICMP dan DNS.

Cangkang nuklir di atas ICMP

Ya, pada tahun 2020 saya mengetahui bahwa Anda dapat memasukkan muatan sewenang-wenang ke dalam paket ICMP. Tapi lebih baik terlambat daripada tidak sama sekali! Dan karena sesuatu dapat dilakukan untuk mengatasinya, maka hal itu perlu dilakukan. Karena dalam keseharian saya paling sering menggunakan command line, termasuk melalui SSH, ide shell ICMP pertama kali muncul di benak saya. Dan untuk menyusun bingo bullshield yang lengkap, saya memutuskan untuk menulisnya sebagai modul Linux dalam bahasa yang hanya saya ketahui secara kasar. Shell seperti itu tidak akan terlihat dalam daftar proses, Anda dapat memuatnya ke dalam kernel dan tidak akan ada di sistem file, Anda tidak akan melihat sesuatu yang mencurigakan dalam daftar port yang mendengarkan. Dalam hal kemampuannya, ini adalah rootkit yang lengkap, tetapi saya berharap dapat memperbaikinya dan menggunakannya sebagai shell pilihan terakhir ketika Load Average terlalu tinggi untuk masuk melalui SSH dan setidaknya menjalankannya echo i > /proc/sysrq-triggeruntuk memulihkan akses tanpa me-reboot.

Kami mengambil editor teks, keterampilan pemrograman dasar dengan Python dan C, Google dan maya yang tidak keberatan Anda lakukan jika semuanya rusak (opsional - VirtualBox/KVM/dll lokal) dan ayo berangkat!

Sisi klien

Bagi saya, untuk bagian klien, saya harus menulis skrip dengan sekitar 80 baris, tetapi ada orang baik yang melakukannya untuk saya. semua pekerjaan. Kode tersebut ternyata sangat sederhana, terdiri dari 10 baris penting:

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

Skrip ini mengambil dua argumen, alamat dan payload. Sebelum dikirim, payload didahului dengan sebuah kunci run:, kita memerlukannya untuk mengecualikan paket dengan muatan acak.

Kernel memerlukan hak istimewa untuk membuat paket, sehingga skrip harus dijalankan sebagai pengguna super. Jangan lupa memberikan izin eksekusi dan menginstal scapy sendiri. Debian memiliki paket bernama python3-scapy. Sekarang Anda dapat memeriksa cara kerjanya.

Menjalankan dan mengeluarkan perintah
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!

Ini penampakannya di 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

Payload dalam paket respons tidak berubah.

Modul kernel

Untuk membangun mesin virtual Debian, Anda memerlukan setidaknya make ΠΈ linux-headers-amd64, sisanya akan datang dalam bentuk ketergantungan. Saya tidak akan memberikan keseluruhan kode di artikel; Anda dapat mengkloningnya di Github.

Pengaturan kait

Untuk memulainya, kita memerlukan dua fungsi untuk memuat modul dan membongkarnya. Fungsi untuk bongkar tidak diperlukan, tapi kemudian rmmod itu tidak akan berfungsi; modul hanya akan dibongkar ketika dimatikan.

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

Apa yang terjadi di sini:

  1. Dua file header ditarik untuk memanipulasi modul itu sendiri dan netfilter.
  2. Semua operasi melalui netfilter, Anda dapat memasang kait di dalamnya. Untuk melakukan ini, Anda perlu mendeklarasikan struktur di mana hook akan dikonfigurasi. Yang paling penting adalah menentukan fungsi yang akan dieksekusi sebagai hook: nfho.hook = icmp_cmd_executor; Saya akan membahas fungsinya nanti.
    Kemudian saya mengatur waktu pemrosesan untuk paket tersebut: NF_INET_PRE_ROUTING menentukan untuk memproses paket ketika pertama kali muncul di kernel. Dapat digunakan NF_INET_POST_ROUTING untuk memproses paket saat keluar dari kernel.
    Saya mengatur filter ke IPv4: nfho.pf = PF_INET;.
    Saya memberikan prioritas tertinggi pada hook saya: nfho.priority = NF_IP_PRI_FIRST;
    Dan saya mendaftarkan struktur data sebagai pengait sebenarnya: nf_register_net_hook(&init_net, &nfho);
  3. Fungsi terakhir menghilangkan pengait.
  4. Lisensinya ditunjukkan dengan jelas sehingga kompiler tidak mengeluh.
  5. Fungsi module_init() ΠΈ module_exit() atur fungsi lain untuk menginisialisasi dan mengakhiri modul.

Mengambil muatan

Sekarang kita perlu mengekstrak payloadnya, ini ternyata menjadi tugas yang paling sulit. Kernel tidak memiliki fungsi bawaan untuk bekerja dengan payload, Anda hanya dapat mengurai header protokol tingkat yang lebih tinggi.

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

Apa yang terjadi:

  1. Saya harus memasukkan file header tambahan, kali ini untuk memanipulasi header IP dan ICMP.
  2. Saya mengatur panjang garis maksimum: #define MAX_CMD_LEN 1976. Mengapa tepatnya ini? Karena kompiler mengeluhkannya! Mereka telah menyarankan kepada saya bahwa saya perlu memahami tumpukan dan heap, suatu hari nanti saya pasti akan melakukan ini dan bahkan mungkin memperbaiki kodenya. Saya segera mengatur baris yang akan berisi perintah: char cmd_string[MAX_CMD_LEN];. Itu harus terlihat di semua fungsi; saya akan membicarakan ini lebih detail di paragraf 9.
  3. Sekarang kita perlu menginisialisasi (struct work_struct my_work;) struktur dan menghubungkannya dengan fungsi lain (DECLARE_WORK(my_work, work_handler);). Saya juga akan membicarakan mengapa hal ini perlu di paragraf kesembilan.
  4. Sekarang saya mendeklarasikan suatu fungsi, yang akan menjadi pengait. Tipe dan argumen yang diterima ditentukan oleh netfilter, kami hanya tertarik pada itu skb. Ini adalah buffer soket, struktur data mendasar yang berisi semua informasi yang tersedia tentang suatu paket.
  5. Agar fungsi dapat berfungsi, Anda memerlukan dua struktur dan beberapa variabel, termasuk dua iterator.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Kita bisa mulai dengan logika. Agar modul dapat berfungsi, tidak diperlukan paket selain ICMP Echo, jadi kami mengurai buffer menggunakan fungsi bawaan dan membuang semua paket non-ICMP dan non-Echo. Kembali NF_ACCEPT berarti penerimaan paket, tetapi Anda juga dapat membatalkan paket dengan mengembalikannya 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;
      }

    Saya belum menguji apa yang akan terjadi tanpa memeriksa header IP. Pengetahuan minimal saya tentang C memberi tahu saya bahwa tanpa pemeriksaan tambahan, sesuatu yang buruk pasti akan terjadi. Saya akan senang jika Anda mencegah saya melakukan hal ini!

  7. Sekarang paket sudah sesuai dengan tipe yang Anda perlukan, Anda dapat mengekstrak datanya. Tanpa fungsi bawaan, Anda harus mendapatkan penunjuk ke awal payload terlebih dahulu. Ini dilakukan di satu tempat, Anda perlu mengarahkan penunjuk ke awal header ICMP dan memindahkannya ke ukuran header ini. Semuanya menggunakan struktur icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Ujung header harus sesuai dengan ujung payload yang masuk skb, oleh karena itu kami memperolehnya menggunakan sarana nuklir dari struktur yang sesuai: tail = skb_tail_pointer(skb);.

    Cangkang nuklir di atas ICMP

    Gambar itu dicuri karenanya, Anda dapat membaca lebih lanjut tentang buffer soket.

  8. Setelah Anda memiliki petunjuk ke awal dan akhir, Anda dapat menyalin data ke dalam string cmd_string, periksa keberadaan awalan run: dan, buang paketnya jika hilang, atau tulis ulang barisnya lagi, hapus awalan ini.
  9. Itu saja, sekarang Anda dapat menghubungi penangan lain: schedule_work(&my_work);. Karena tidak mungkin meneruskan parameter ke panggilan seperti itu, baris dengan perintah harus bersifat global. schedule_work() akan menempatkan fungsi yang terkait dengan struktur yang diteruskan ke dalam antrian umum penjadwal tugas dan selesai, sehingga Anda tidak perlu menunggu perintah selesai. Hal ini diperlukan karena pengaitnya harus sangat cepat. Jika tidak, pilihan Anda adalah tidak ada yang dimulai atau Anda akan mengalami kepanikan kernel. Penundaan itu seperti kematian!
  10. Itu saja, Anda dapat menerima paket dengan pengembalian yang sesuai.

Memanggil program di ruang pengguna

Fungsi ini adalah yang paling dimengerti. Namanya diberikan DECLARE_WORK(), tipe dan argumen yang diterima tidak menarik. Kami mengambil baris dengan perintah dan meneruskannya sepenuhnya ke shell. Biarkan dia menangani parsing, mencari binari, dan lainnya.

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. Atur argumen ke array string argv[]. Saya berasumsi bahwa semua orang tahu bahwa program sebenarnya dijalankan dengan cara ini, dan bukan sebagai garis kontinu dengan spasi.
  2. Tetapkan variabel lingkungan. Saya hanya memasukkan PATH dengan kumpulan jalur minimum, berharap semuanya sudah digabungkan /bin с /usr/bin и /sbin с /usr/sbin. Jalur lain jarang menjadi masalah dalam praktiknya.
  3. Selesai, ayo kita lakukan! Fungsi kernel call_usermodehelper() menerima masuk. jalur ke biner, larik argumen, larik variabel lingkungan. Di sini saya juga berasumsi bahwa semua orang memahami arti meneruskan jalur ke file yang dapat dieksekusi sebagai argumen terpisah, tetapi Anda dapat bertanya. Argumen terakhir menentukan apakah akan menunggu hingga proses selesai (UMH_WAIT_PROC), proses dimulai (UMH_WAIT_EXEC) atau tidak menunggu sama sekali (UMH_NO_WAIT). Apakah masih ada lagi UMH_KILLABLE, saya tidak memeriksanya.

Majelis

Perakitan modul kernel dilakukan melalui kerangka kerja kernel. Ditelepon make di dalam direktori khusus yang terikat dengan versi kernel (didefinisikan di sini: KERNELDIR:=/lib/modules/$(shell uname -r)/build), dan lokasi modul diteruskan ke variabel M dalam argumen. Target icmpshell.ko dan clean menggunakan kerangka kerja ini sepenuhnya. DI DALAM obj-m menunjukkan file objek yang akan diubah menjadi modul. Sintaks yang dibuat ulang main.o Π² icmpshell.o (icmpshell-objs = main.o) tidak terlihat logis bagi saya, tapi biarlah.

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

Kami mengumpulkan: make. Memuat: insmod icmpshell.ko. Selesai, Anda dapat memeriksa: sudo ./send.py 45.11.26.232 "date > /tmp/test". Jika Anda memiliki file di mesin Anda /tmp/test dan itu berisi tanggal pengiriman permintaan, yang berarti Anda melakukan semuanya dengan benar dan saya melakukan semuanya dengan benar.

Kesimpulan

Pengalaman pertama saya dengan pengembangan nuklir jauh lebih mudah dari yang saya perkirakan. Bahkan tanpa pengalaman mengembangkan di C, dengan fokus pada petunjuk kompiler dan hasil Google, saya dapat menulis modul yang berfungsi dan merasa seperti seorang peretas kernel, dan pada saat yang sama seorang skrip kiddie. Selain itu, saya pergi ke saluran Kernel Newbies, di mana saya diberitahu untuk menggunakannya schedule_work() alih-alih menelepon call_usermodehelper() di dalam kail itu sendiri dan mempermalukannya, dengan tepat mencurigai adanya penipuan. Seratus baris kode menghabiskan waktu sekitar satu minggu pengembangan di waktu luang saya. Pengalaman sukses yang menghancurkan mitos pribadi saya tentang kompleksitas pengembangan sistem yang luar biasa.

Jika ada yang setuju untuk melakukan review kode di Github, saya akan berterima kasih. Saya cukup yakin saya membuat banyak kesalahan bodoh, terutama saat bekerja dengan string.

Cangkang nuklir di atas ICMP

Sumber: www.habr.com

Tambah komentar