Vỏ hạt nhân trên ICMP

Vỏ hạt nhân trên ICMP

TL; DR: Tôi đang viết một mô-đun hạt nhân sẽ đọc các lệnh từ tải trọng ICMP và thực thi chúng trên máy chủ ngay cả khi SSH của bạn gặp sự cố. Đối với những người thiếu kiên nhẫn nhất, tất cả mã đều được github.

Thận trọng Lập trình viên C có kinh nghiệm có nguy cơ bật khóc! Tôi thậm chí có thể sai trong thuật ngữ, nhưng mọi lời chỉ trích đều được hoan nghênh. Bài viết dành cho những người có ý tưởng sơ bộ về lập trình C và muốn tìm hiểu sâu hơn về bên trong Linux.

Trong phần bình luận đầu tiên của tôi Bài viết đã đề cập đến SoftEther VPN, có thể bắt chước một số giao thức “thông thường”, đặc biệt là HTTPS, ICMP và thậm chí cả DNS. Tôi có thể tưởng tượng chỉ có cái đầu tiên trong số chúng hoạt động, vì tôi rất quen thuộc với HTTP(S) và tôi phải học cách tạo đường hầm qua ICMP và DNS.

Vỏ hạt nhân trên ICMP

Có, vào năm 2020, tôi đã biết rằng bạn có thể chèn tải trọng tùy ý vào gói ICMP. Nhưng thà có còn hơn không! Và vì có thể làm được điều gì đó nên nó cần phải được thực hiện. Vì trong cuộc sống hàng ngày, tôi thường sử dụng dòng lệnh nhất, bao gồm cả thông qua SSH, nên ý tưởng về ICMP shell xuất hiện trong đầu tôi đầu tiên. Và để lắp ráp một trò chơi lô tô bullshield hoàn chỉnh, tôi quyết định viết nó dưới dạng mô-đun Linux bằng ngôn ngữ mà tôi chỉ có ý tưởng sơ bộ. Shell như vậy sẽ không hiển thị trong danh sách các tiến trình, bạn có thể tải nó vào kernel và nó sẽ không có trên hệ thống tệp, bạn sẽ không thấy bất kỳ điều gì đáng ngờ trong danh sách các cổng nghe. Về khả năng của nó, đây là một rootkit chính thức, nhưng tôi hy vọng sẽ cải thiện nó và sử dụng nó như một giải pháp cuối cùng khi Tải trung bình quá cao để đăng nhập qua SSH và thực thi ít nhất echo i > /proc/sysrq-triggerđể khôi phục quyền truy cập mà không cần khởi động lại.

Chúng tôi có trình soạn thảo văn bản, kỹ năng lập trình cơ bản bằng Python và C, Google và ảo mà bạn không ngại đặt dao nếu mọi thứ bị hỏng (tùy chọn - VirtualBox/KVM/v.v. cục bộ) và bắt đầu!

Phía khách hàng

Đối với tôi, có vẻ như đối với phần khách hàng, tôi sẽ phải viết một kịch bản khoảng 80 dòng, nhưng đã có những người tốt bụng làm việc đó cho tôi. Tất cả công việc. Đoạn mã hóa ra đơn giản đến không ngờ, chỉ gồm 10 dòng quan trọng:

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

Tập lệnh có hai đối số, một địa chỉ và tải trọng. Trước khi gửi, tải trọng được đặt trước bởi một khóa run:, chúng tôi sẽ cần nó để loại trừ các gói có tải trọng ngẫu nhiên.

Kernel yêu cầu đặc quyền để tạo các gói, vì vậy tập lệnh sẽ phải được chạy dưới dạng siêu người dùng. Đừng quên cấp quyền thực thi và cài đặt scapy. Debian có một gói tên là python3-scapy. Bây giờ bạn có thể kiểm tra xem tất cả hoạt động như thế nào.

Chạy và xuất lệnh
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!

Đây là những gì nó trông giống như trong 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

Tải trọng trong gói phản hồi không thay đổi.

mô-đun hạt nhân

Để xây dựng một máy ảo Debian, bạn sẽ cần ít nhất make и linux-headers-amd64, phần còn lại sẽ ở dạng phụ thuộc. Tôi sẽ không cung cấp toàn bộ mã trong bài viết; bạn có thể sao chép nó trên Github.

Thiết lập móc

Để bắt đầu, chúng ta cần hai hàm để tải và dỡ mô-đun. Chức năng dỡ hàng là không cần thiết, nhưng sau đó rmmod nó sẽ không hoạt động; mô-đun sẽ chỉ được tải khi tắt.

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

Những gì đang xảy ra ở đây:

  1. Hai tệp tiêu đề được kéo vào để thao tác với chính mô-đun và bộ lọc mạng.
  2. Tất cả các hoạt động đều thông qua bộ lọc mạng, bạn có thể đặt móc nối trong đó. Để làm điều này, bạn cần khai báo cấu trúc mà hook sẽ được cấu hình. Điều quan trọng nhất là chỉ định hàm sẽ được thực thi dưới dạng hook: nfho.hook = icmp_cmd_executor; Tôi sẽ tìm hiểu chức năng này sau.
    Sau đó tôi đặt thời gian xử lý cho gói: NF_INET_PRE_ROUTING chỉ định xử lý gói khi nó xuất hiện lần đầu trong kernel. Có thể được sử dụng NF_INET_POST_ROUTING để xử lý gói khi nó thoát khỏi kernel.
    Tôi đặt bộ lọc thành IPv4: nfho.pf = PF_INET;.
    Tôi ưu tiên cao nhất cho hook của mình: nfho.priority = NF_IP_PRI_FIRST;
    Và tôi đăng ký cấu trúc dữ liệu làm hook thực tế: nf_register_net_hook(&init_net, &nfho);
  3. Hàm cuối cùng sẽ loại bỏ hook.
  4. Giấy phép được chỉ định rõ ràng để trình biên dịch không phàn nàn.
  5. Chức năng module_init() и module_exit() thiết lập các chức năng khác để khởi tạo và kết thúc mô-đun.

Lấy tải trọng

Bây giờ chúng ta cần trích xuất tải trọng, đây hóa ra lại là nhiệm vụ khó khăn nhất. Hạt nhân không có các hàm dựng sẵn để làm việc với tải trọng; bạn chỉ có thể phân tích các tiêu đề của các giao thức cấp cao hơn.

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

Điều gì đang xảy ra:

  1. Tôi đã phải đưa vào các tệp tiêu đề bổ sung, lần này để thao tác các tiêu đề IP và ICMP.
  2. Tôi đặt độ dài dòng tối đa: #define MAX_CMD_LEN 1976. Tại sao chính xác điều này? Bởi vì trình biên dịch phàn nàn về điều đó! Họ đã gợi ý cho tôi rằng tôi cần phải hiểu về stack và heap, một ngày nào đó tôi chắc chắn sẽ làm được điều này và thậm chí có thể sửa mã. Tôi đặt ngay dòng sẽ chứa lệnh: char cmd_string[MAX_CMD_LEN];. Nó sẽ hiển thị trong tất cả các chức năng; tôi sẽ nói chi tiết hơn về điều này trong đoạn 9.
  3. Bây giờ chúng ta cần khởi tạo (struct work_struct my_work;) cấu trúc và kết nối nó với một chức năng khác (DECLARE_WORK(my_work, work_handler);). Tôi cũng sẽ nói về lý do tại sao điều này lại cần thiết trong đoạn thứ chín.
  4. Bây giờ tôi khai báo một hàm, nó sẽ là một cái hook. Loại và đối số được chấp nhận được quyết định bởi bộ lọc mạng, chúng tôi chỉ quan tâm đến skb. Đây là bộ đệm ổ cắm, cấu trúc dữ liệu cơ bản chứa tất cả thông tin có sẵn về gói.
  5. Để hàm hoạt động, bạn sẽ cần hai cấu trúc và một số biến, bao gồm cả hai biến lặp.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Chúng ta có thể bắt đầu bằng logic. Để mô-đun hoạt động, không cần gói nào khác ngoài ICMP Echo, vì vậy chúng tôi phân tích bộ đệm bằng cách sử dụng các hàm tích hợp và loại bỏ tất cả các gói không phải ICMP và không phải Echo. Trở lại NF_ACCEPT có nghĩa là chấp nhận gói hàng, nhưng bạn cũng có thể bỏ gói hàng bằng cách trả lại 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;
      }

    Tôi chưa kiểm tra điều gì sẽ xảy ra nếu không kiểm tra tiêu đề IP. Kiến thức tối thiểu về C của tôi cho tôi biết rằng nếu không có những kiểm tra bổ sung, điều gì đó khủng khiếp chắc chắn sẽ xảy ra. Tôi sẽ rất vui nếu bạn can ngăn tôi về điều này!

  7. Bây giờ gói đã đúng loại bạn cần, bạn có thể trích xuất dữ liệu. Nếu không có chức năng tích hợp sẵn, trước tiên bạn phải lấy con trỏ đến phần đầu của tải trọng. Việc này được thực hiện ở một nơi, bạn cần đưa con trỏ đến đầu tiêu đề ICMP và di chuyển nó đến kích thước của tiêu đề này. Mọi thứ đều sử dụng cấu trúc icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Phần cuối của tiêu đề phải khớp với phần cuối của tải trọng trong skb, do đó chúng tôi thu được nó bằng phương tiện hạt nhân từ cấu trúc tương ứng: tail = skb_tail_pointer(skb);.

    Vỏ hạt nhân trên ICMP

    Bức ảnh đã bị đánh cắp do đó, bạn có thể đọc thêm về bộ đệm ổ cắm.

  8. Khi đã có con trỏ đến đầu và cuối, bạn có thể sao chép dữ liệu thành một chuỗi cmd_string, kiểm tra sự hiện diện của tiền tố run: và loại bỏ gói nếu nó bị thiếu hoặc viết lại dòng, xóa tiền tố này.
  9. Thế là xong, bây giờ bạn có thể gọi một trình xử lý khác: schedule_work(&my_work);. Vì không thể truyền tham số cho lệnh gọi như vậy nên dòng có lệnh phải là toàn cục. schedule_work() sẽ đặt chức năng được liên kết với cấu trúc được truyền vào hàng đợi chung của bộ lập lịch tác vụ và hoàn thành, cho phép bạn không phải đợi lệnh hoàn thành. Điều này là cần thiết vì móc phải rất nhanh. Nếu không, lựa chọn của bạn là sẽ không có gì bắt đầu hoặc bạn sẽ bị hoảng loạn kernel. Trì hoãn giống như cái chết!
  10. Thế là xong, bạn có thể chấp nhận gói hàng với mức hoàn trả tương ứng.

Gọi một chương trình trong không gian người dùng

Chức năng này là dễ hiểu nhất. Tên của nó đã được đưa ra trong DECLARE_WORK(), loại và đối số được chấp nhận không thú vị. Chúng tôi lấy dòng lệnh và chuyển nó hoàn toàn vào trình bao. Hãy để anh ấy xử lý việc phân tích cú pháp, tìm kiếm các tệp nhị phân và mọi thứ khác.

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. Đặt đối số thành một chuỗi các chuỗi argv[]. Tôi giả định rằng mọi người đều biết rằng các chương trình thực sự được thực thi theo cách này chứ không phải dưới dạng một dòng liên tục có khoảng trắng.
  2. Đặt các biến môi trường. Tôi chỉ chèn PATH với một nhóm đường dẫn tối thiểu, hy vọng rằng tất cả chúng đã được kết hợp /bin с /usr/bin и /sbin с /usr/sbin. Những con đường khác hiếm khi quan trọng trong thực tế.
  3. Xong rồi, hãy làm thôi! chức năng hạt nhân call_usermodehelper() chấp nhận nhập cảnh. đường dẫn đến nhị phân, mảng đối số, mảng biến môi trường. Ở đây tôi cũng giả định rằng mọi người đều hiểu ý nghĩa của việc truyền đường dẫn đến file thực thi dưới dạng đối số riêng biệt, nhưng bạn có thể hỏi. Đối số cuối cùng chỉ định có nên đợi quá trình hoàn tất hay không (UMH_WAIT_PROC), quá trình bắt đầu (UMH_WAIT_EXEC) hoặc không đợi chút nào (UMH_NO_WAIT). Có thêm nữa không UMH_KILLABLE, Tôi đã không nhìn vào nó.

Lắp ráp

Việc lắp ráp các mô-đun hạt nhân được thực hiện thông qua khung tạo hạt nhân. Gọi điện make bên trong một thư mục đặc biệt gắn với phiên bản kernel (được định nghĩa ở đây: KERNELDIR:=/lib/modules/$(shell uname -r)/build) và vị trí của mô-đun được truyền cho biến M trong các lập luận. icmpshell.ko và các mục tiêu sạch hoàn toàn sử dụng khung này. TRONG obj-m cho biết tệp đối tượng sẽ được chuyển đổi thành mô-đun. Cú pháp làm lại main.o в icmpshell.o (icmpshell-objs = main.o) đối với tôi có vẻ không logic lắm, nhưng cứ như vậy đi.

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

Chúng tôi thu thập: make. Đang tải: insmod icmpshell.ko. Xong, bạn có thể kiểm tra: sudo ./send.py 45.11.26.232 "date > /tmp/test". Nếu bạn có một tập tin trên máy của bạn /tmp/test và nó chứa ngày gửi yêu cầu, có nghĩa là bạn đã làm đúng mọi thứ và tôi đã làm đúng mọi thứ.

Kết luận

Trải nghiệm đầu tiên của tôi về phát triển hạt nhân dễ dàng hơn nhiều so với tôi mong đợi. Ngay cả khi không có kinh nghiệm phát triển bằng C, tập trung vào các gợi ý của trình biên dịch và kết quả của Google, tôi vẫn có thể viết một mô-đun hoạt động và cảm thấy mình giống như một hacker hạt nhân, đồng thời là một đứa trẻ viết kịch bản. Ngoài ra, tôi đã truy cập kênh Kernel Newbies, nơi tôi được yêu cầu sử dụng schedule_work() thay vì gọi call_usermodehelper() vào bên trong cái móc và làm anh ta xấu hổ, nghi ngờ một cách đúng đắn là một trò lừa đảo. Một trăm dòng mã khiến tôi mất khoảng một tuần phát triển trong thời gian rảnh. Một trải nghiệm thành công đã phá hủy huyền thoại cá nhân của tôi về sự phức tạp quá mức của việc phát triển hệ thống.

Nếu ai đó đồng ý thực hiện đánh giá mã trên Github, tôi sẽ rất biết ơn. Tôi khá chắc rằng mình đã mắc rất nhiều lỗi ngu ngốc, đặc biệt là khi làm việc với chuỗi.

Vỏ hạt nhân trên ICMP

Nguồn: www.habr.com

Thêm một lời nhận xét