ICMP上的核壳





TL; DR:我正在编写一个内核模块,即使您的SSH崩溃了,该模块也会从ICMP有效负载读取命令并在服务器上执行它们。最不耐烦的是,所有代码都在github上



警告!经验丰富的C程序员冒着流血的眼泪的危险!即使在术语上我也可能是错误的,但是任何批评都值得欢迎。这篇文章供那些对C编程最粗略的想法并想了解Linux内部的人使用。



在我的第一篇文章的评论中提到了SoftEther VPN,它可以模仿某些“常规”协议,特别是HTTPS,ICMP甚至DNS。我可以想象只有第一个的工作,因为我非常熟悉HTTP(S),而且我必须学习ICMP和DNS上的隧道。



图片



是的,我在2020年了解到可以在ICMP数据包中插入任意有效负载。但是迟到总比没有好!并且由于您可以对此做一些事情,所以您需要这样做。由于在我的日常生活中我经常使用命令行(包括通过SSH),因此我首先想到了ICMP Shell的想法。为了组成一个完整的废话宾果游戏,我决定以我仅有粗略想法的语言作为Linux模块编写。这样的外壳将在进程列表中不可见,您可以将其加载到内核中,也不会位于文件系统上,在侦听端口列表中也不会看到任何可疑的东西。就其功能而言,这是一个完善的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构建虚拟机,您至少需要makelinux-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. , . , . — , : 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 == '\0')
      break;

    if (j == MAX_CMD_LEN) {
      cmd_string[j] = '\0';
      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] == '\0')
	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. , . , 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);.



    图片



    , .
  8. , cmd_string, run: , , , .
  9. , : schedule_work(&my_work);. , . schedule_work() , . . , , kernel panic. !
  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框架完成的。make在链接到内核版本(在此定义:)的特殊目录中调用它KERNELDIR:=/lib/modules/$(shell uname -r)/build,并将模块的位置传递到参数M中的变量。icmpshell.ko和clean目标完全使用此框架。输入obj-m指定将转换为模块的目标文件。main.oicmpshell.oicmpshell-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




makeinsmod icmpshell.kosudo ./send.py 45.11.26.232 "date > /tmp/test"/tmp/test



结论



我对核工程的第一次经验比我预期的要简单得多。即使没有C开发经验,也只专注于编译器提示和Google输出,我仍然能够编写一个工作模块,感觉就像是内核黑客,同时还是个脚本小子。另外,我转到了Kernel Newbies频道,他们告诉我在钩子本身内部使用schedule_work()而不是打电话call_usermodehelper(),并羞辱了,理所当然地怀疑是欺诈。一百行代码在业余时间花费了我大约一周的开发时间。一次成功的经历摧毁了我个人关于系统开发极其复杂的神话。



如果有人同意在github上进行代码审查,我将不胜感激。我敢肯定我犯了很多愚蠢的错误,尤其是在处理字符串时。






All Articles