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 .
Perhatian! Para programmer C berpengalaman berisiko meneteskan air mata darah! Saya mungkin salah bahkan dalam terminologinya, tetapi kritik apa pun diterima. Postingan ini ditujukan bagi mereka yang hanya memiliki pemahaman dasar tentang pemrograman C dan ingin melihat lebih dalam. Linux.
Di komentar pertama saya 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.

Ya, saya baru tahu di tahun 2020 bahwa kita bisa menyisipkan payload sembarangan ke dalam paket ICMP. Tapi lebih baik terlambat daripada tidak sama sekali! Dan jika ada yang bisa dilakukan, maka itu harus dilakukan. Karena saya kebanyakan menggunakan baris perintah dalam pekerjaan sehari-hari, termasuk melalui SSH, ide tentang shell ICMP adalah hal pertama yang terlintas di pikiran saya. Dan untuk membangun bullshield bingo yang lengkap, saya memutuskan untuk menuliskannya sebagai sebuah modul. Linux dalam bahasa yang hanya saya pahami secara kasar. Shell semacam itu tidak akan terlihat dalam daftar proses, dapat dimuat ke dalam kernel dan tidak akan berada di sistem file, dan Anda tidak akan melihat sesuatu yang mencurigakan dalam daftar port yang mendengarkan. Ini adalah rootkit yang lengkap dalam kemampuannya, tetapi saya berharap untuk menyempurnakannya dan menggunakannya sebagai shell pilihan terakhir ketika Load Average terlalu tinggi untuk masuk melalui SSH dan melakukan tugas-tugas dasar sekalipun. echo i > /proc/sysrq-triggeruntuk memulihkan akses tanpa me-reboot.
Kami mengambil editor teks, keterampilan pemrograman dasar dengan Python dan C, Google dan 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. . 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 akses untuk membuat paket, jadi skrip perlu dijalankan sebagai root. Jangan lupa untuk memberikan izin eksekusi dan menginstal scapy itu sendiri. Debian ada sebuah 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 jalankan:Halo, dunia
0010 21!
Data: 72756e3a48656c6c6f2c20776f726c6421
[Panjang: 17]
Frame 2: 59 byte pada kabel (472 bit), 59 byte ditangkap (472 bit) pada antarmuka wlp1s0, id 0
Protokol Internet Versi 4, Sumber: 45.11.26.232, Tujuan: 192.168.0.240
Protokol Pesan Kontrol Internet
Tipe: 0 (Balasan gema (ping))
Kode: 0
Checksum: 0xde03 [benar]
[Status Checksum: Baik]
Pengidentifikasi (BE): 0 (0x0000)
Pengidentifikasi (LE): 0 (0x0000)
Nomor urut (BE): 0 (0x0000)
Nomor urut (LE): 0 (0x0000)
[Bingkai permintaan: 1]
[Waktu respons: 19.094 ms]
Data (17 byte)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 jalankan:Halo, dunia
0010 21!
Data: 72756e3a48656c6c6f2c20776f726c6421
[Panjang: 17]
Paket ^C2 ditangkap
Payload dalam paket respons tidak berubah.
Modul kernel
Untuk membangun di mesin virtual dengan Debian setidaknya membutuhkan 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:
- Dua file header ditarik untuk memanipulasi modul itu sendiri dan netfilter.
- 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_ROUTINGmenentukan untuk memproses paket ketika pertama kali muncul di kernel. Dapat digunakanNF_INET_POST_ROUTINGuntuk 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); - Fungsi terakhir menghilangkan pengait.
- Lisensinya ditunjukkan dengan jelas sehingga kompiler tidak mengeluh.
- 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:
- Saya harus memasukkan file header tambahan, kali ini untuk memanipulasi header IP dan ICMP.
- 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. - 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. - 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. - 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; - 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_ACCEPTberarti penerimaan paket, tetapi Anda juga dapat membatalkan paket dengan mengembalikannyaNF_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!
- 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 masukskb, oleh karena itu kami memperolehnya menggunakan sarana nuklir dari struktur yang sesuai:tail = skb_tail_pointer(skb);.
Gambar itu dicuri , Anda dapat membaca lebih lanjut tentang buffer soket. - Setelah Anda memiliki petunjuk ke awal dan akhir, Anda dapat menyalin data ke dalam string
cmd_string, periksa keberadaan awalanrun:dan, buang paketnya jika hilang, atau tulis ulang barisnya lagi, hapus awalan ini. - 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! - 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);
}- 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. - 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. - 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 lagiUMH_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
objek-m = icmpshell.o
icmpshell-objs = main.o
semua: icmpshell.ko
icmpshell.ko:main.c
buat -C $(KERNELDIR) M=$(PWD) modul
bersih:
buat -C $(KERNELDIR) M=$(PWD) bersih
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.
Sumber: www.habr.com

