ICMP 上的核殼

ICMP 上的核殼

TL博士:我正在編寫一個核心模組,即使您的 SSH 崩潰,它也會從 ICMP 負載中讀取命令並在伺服器上執行它們。 對於最不耐煩的人來說,所有程式碼都是 GitHub上.

注意! 經驗豐富的 C 程式設計師可能會熱淚盈眶! 我甚至可能在術語上是錯誤的,但歡迎任何批評。 這篇文章是針對那些對 C 程式設計有非常粗略了解並想要深入了解 Linux 內部的人的。

在我的第一條評論中 文章 提到 SoftEther VPN,它可以模仿一些「常規」協議,特別是 HTTPS、ICMP 甚至 DNS。 我可以想像只有第一個可以工作,因為我非常熟悉 HTTP(S),而且我必須學習 ICMP 和 DNS 上的隧道。

ICMP 上的核殼

是的,在 2020 年,我了解到可以在 ICMP 封包中插入任意有效負載。 但遲到總比不到好! 既然可以對此採取行動,那麼就需要採取行動。 由於在日常生活中我最常使用命令列,包括透過 SSH,因此我首先想到了 ICMP shell 的想法。 為了組裝一個完整的 bullshield bingo,我決定用一種我只是粗略了解的語言將其編寫為 Linux 模組。 這樣的 shell 在進程列表中將不可見,您可以將其載入到核心中,並且它不會在檔案系統上,您不會在偵聽連接埠清單中看到任何可疑的內容。 就其功能而言,這是一個成熟的rootkit,但我希望改進它,並將其用作平均負載太高而無法通過SSH登錄並至少執行時的最後手段 echo i > /proc/sysrq-trigger無需重新啟動即可恢復存取。

我們需要一個文字編輯器、Python 和 C 的基本程式設計技能、Google 和 虛擬的 如果一切都壞了,你不介意把它放在刀下(可選 - 本地 VirtualBox/KVM/等),然後開始吧!

客戶端

在我看來,對於客戶端部分,我必須編寫一個大約 80 行的腳本,但是有好心的人為我做了這件事 所有的工作。 程式碼出乎意料地簡單,只有 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()

此腳本採用兩個參數:一個位址和一個有效負載。 發送之前,有效負載前面有一個密鑰 run:,我們將需要它來排除具有隨機負載的套件。

核心需要權限來製作包,因此腳本必須以超級用戶身份運行。 不要忘記授予執行權限並安裝 scapy 本身。 Debian 有一個名為 python3-scapy。 現在您可以檢查這一切是如何運作的。

運行並輸出命令
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!

這就是嗅探器裡的樣子
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

響應包中的負載不會改變。

核心模組

要建立 Debian 虛擬機,您至少需要 make и linux-headers-amd64,其餘的將以依賴關係的形式出現。 我不會在文章中提供完整的程式碼;您可以在 Github 上克隆它。

掛鉤設置

首先,我們需要兩個函數來載入和卸載模組。 卸載的功能不是必須的,但是接下來 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. 所有的操作都會經過一個netfilter,你可以在裡面設定hooks。 為此,您需要聲明將在其中配置掛鉤的結構。 最重要的是指定將作為鉤子執行的函數: nfho.hook = icmp_cmd_executor; 稍後我將討論該函數本身。
    然後我設定包裹的處理時間: NF_INET_PRE_ROUTING 指定當套件第一次出現在內核中時對其進行處理。 可以使用 NF_INET_POST_ROUTING 在資料包退出核心時進行處理。
    我將過濾器設定為 IPv4: nfho.pf = PF_INET;.
    我給我的鉤子最高優先: nfho.priority = NF_IP_PRI_FIRST;
    我將資料結構註冊為實際的鉤子: nf_register_net_hook(&init_net, &nfho);
  3. 最後一個函數刪除了鉤子。
  4. 明確指出了許可證,以便編譯器不會抱怨。
  5. 功能 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;
}

發生了什麼事:

  1. 我必須包含額外的頭文件,這次是為了操作 IP 和 ICMP 標頭。
  2. 我設定最大行長度: #define MAX_CMD_LEN 1976。 究竟是為什麼呢? 因為編譯器會抱怨它! 他們已經向我建議我需要了解堆疊和堆,有一天我肯定會這樣做,甚至可能會更正程式碼。 我立即設定了包含該命令的行: char cmd_string[MAX_CMD_LEN];。 它應該在所有函數中可見;我將在第 9 段中更詳細地討論這一點。
  3. 現在我們需要初始化(struct work_struct my_work;)構造並將其與另一個函數(DECLARE_WORK(my_work, work_handler);)。 我也會在第九段談到為什麼這是必要的。
  4. 現在我聲明一個函數,它將是一個鉤子。 類型和接受的參數由 netfilter 決定,我們只感興趣 skb。 這是一個套接字緩衝區,一個基本資料結構,包含有關資料包的所有可用資訊。
  5. 為了使函數正常工作,您將需要兩個結構和幾個變量,包括兩個迭代器。
      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 表示已接受包裹,但您也可以透過退貨來丟棄包裹 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 標頭會發生什麼。 我對 C 語言的了解告訴我,如果沒有額外的檢查,就一定會發生可怕的事情。 如果你勸阻我這樣做,我會很高興!

  7. 既然包的類型正是您需要的,您就可以提取資料了。 如果沒有內建函數,您首先必須取得指向有效負載開頭的指標。 這是在一個地方完成的,您需要將指標指向 ICMP 標頭的開頭,並將其移至該標頭的大小。 一切都使用結構 icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    標頭的結尾必須與有效負荷的結尾相符 skb,因此我們使用核手段從相應的結構中獲得它: tail = skb_tail_pointer(skb);.

    ICMP 上的核殼

    圖片被偷了 ,您可以閱讀有關套接字緩衝區的更多資訊。

  8. 一旦有了指向開頭和結尾的指針,就可以將資料複製到字串中 cmd_string,檢查是否存在前綴 run: 並且,如果包遺失,則丟棄該包,或再次重寫該行,刪除此前綴。
  9. 就是這樣,現在您可以呼叫另一個處理程序: schedule_work(&my_work);。 由於無法將參數傳遞給此類調用,因此包含命令的行必須是全域的。 schedule_work() 會將與傳遞的結構關聯的函數放入任務排程器的通用佇列中並完成,使您無需等待命令完成。 這是必要的,因為鉤子必須非常快。 否則,您的選擇是什麼都不會啟動,或者您將遇到核心恐慌。 拖延等於死亡!
  10. 就這樣,您可以接受帶有相應退貨的包裹。

呼叫用戶空間中的程式

這個函數是最容易理解的。 它的名字是在 DECLARE_WORK(),類型和接受的參數並不有趣。 我們使用命令並將其完全傳遞給 shell。 讓他處理解析、搜尋二進位檔案和其他所有事情。

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. 將參數設定為字串數組 argv[]。 我假設每個人都知道程式實際上是這樣執行的,而不是作為帶有空格的連續線。
  2. 設定環境變數。 我只插入了具有最小路徑集的 PATH,希望它們都已經組合在一起 /bin с /usr/bin и /sbin с /usr/sbin。 其他路徑在實踐中很少起作用。
  3. 完成了,我們開始吧! 內核函數 call_usermodehelper() 接受條目。 二進位檔案的路徑、參數數組、環境變數數組。 這裡我也假設每個人都理解將可執行檔的路徑作為單獨的參數傳遞的含義,但是你可以問。 最後一個參數指定是否等待進程完成(UMH_WAIT_PROC),進程開始(UMH_WAIT_EXEC)或根本不等待(UMH_NO_WAIT)。 還有還有嗎 UMH_KILLABLE,我沒仔細看。

裝配

內核模組的組裝是透過內核make-framework 來執行的。 被稱為 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

我們收集: make。 載入中: insmod icmpshell.ko。 完成後,您可以檢查: sudo ./send.py 45.11.26.232 "date > /tmp/test"。 如果您的機器上有文件 /tmp/test 它包含請求發送的日期,這意味著你所做的一切都是正確的,我所做的一切都是正確的。

結論

我的第一次核子開發經驗比我預想的要容易得多。 即使沒有 C 語言開發經驗,專注於編譯器提示和 Google 結果,我也能夠編寫一個工作模組,感覺像一個內核黑客,同時也是一個腳本小子。 另外,我去了 Kernel Newbies 頻道,在那裡我被告知要使用 schedule_work() 而不是打電話 call_usermodehelper() 進入鉤子本身並羞辱他,正確地懷疑這是一個騙局。 一百行程式碼花了我大約一週的空閒時間進行開發。 一次成功的經驗打破了我個人對系統開發極為複雜的迷思。

如果有人同意在 Github 上進行程式碼審查,我將不勝感激。 我很確定我犯了很多愚蠢的錯誤,尤其是在使用字串時。

ICMP 上的核殼

來源: www.habr.com

添加評論