Nuclear shell over ICMP

Nuclear shell over ICMP

TL; DR: I am writing a kernel module that will read commands from the ICMP payload and execute them on the server even if your SSH is down. For the most impatient, all the code on github.

Caution! Experienced C programmers risk tears of blood! I may be wrong even in the terminology, but any criticism is welcome. The post is intended for those who have a very rough idea of ​​C programming and want to look into the innards of Linux.

In the comments to my first article they mentioned SoftEther VPN, which can mimic some "normal" protocols, in particular, HTTPS, ICMP and even DNS. I imagine only the first of them working, as I am familiar with HTTP(S), and tunneling over ICMP and DNS had to be studied.

Nuclear shell over ICMP

Yes, I learned in 2020 that you can insert an arbitrary payload into ICMP packets. But better late than never! And if you can do something about it, then you need to do it. Since in my daily life I most often use the command line, including via SSH, the idea of ​​​​an ICMP shell came to my mind first. And in order to put together a complete bullshit bingo, I decided to write as a Linux module in a language that I have only a rough idea about. Such a shell will not be visible in the list of processes, you can load it into the kernel and it will not lie on the file system, you will not see anything suspicious in the list of listening ports. It's a full rootkit in terms of its capabilities, but I hope to refine it and use it as a shell of last resort when the Load Average is too high to SSH in and execute at least echo i > /proc/sysrq-triggerto regain access without rebooting.

We take a text editor, basic programming skills in Python and C, Google and virtual machine which is not a pity to put under the knife if everything breaks down (optionally - local VirtualBox / KVM / etc) and let's go!

Client part

It seemed to me that for the client part I would have to write a script of 80 lines of commercials, but there were kind people who did it for me all work. The code turned out to be unexpectedly simple, fits in 10 meaningful lines:

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

The script takes two arguments, an address and a payload. Before sending, the payload is preceded by a key run:, we will need it to exclude packets with a random payload.

The kernel requires privileges to craft packages, so the script will need to be run as root. Don't forget to give execute permissions and install scapy itself. Debian has a package called python3-scapy. Now you can check how it all works.

Run and command output
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!

This is how it looks in the 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

The payload in the response packet does not change.

kernel module

To build in a virtual machine with Debian, you will need at least make ΠΈ linux-headers-amd64, the rest will be pulled up in the form of dependencies. I will not give the entire code in the article, you can clone it on github.

Hook setup

First we need two functions to load the module and to unload it. The function for unloading is not required, but then rmmod it will not work, the module will be unloaded only when it is turned off.

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

What's going on here:

  1. Two header files are pulled up for manipulations with the module itself and with the netfilter.
  2. All operations go through a netfilter, you can set hooks in it. To do this, you need to declare the structure in which the hook will be configured. The most important thing is to specify the function that will be executed as a hook: nfho.hook = icmp_cmd_executor; I'll get to the function itself.
    Then I set the packet processing time: NF_INET_PRE_ROUTING tells the package to be processed when it first appears in the kernel. Can be used NF_INET_POST_ROUTING to process the packet on exit from the kernel.
    I hang a filter on IPv4: nfho.pf = PF_INET;.
    I assign the highest priority to my hook: nfho.priority = NF_IP_PRI_FIRST;
    And I register the data structure as the actual hook: nf_register_net_hook(&init_net, &nfho);
  3. In the final function, the hook is removed.
  4. The license is indicated explicitly so that the compiler does not swear.
  5. Functions module_init() ΠΈ module_exit() define other functions to initialize and terminate the module.

Payload Extraction

Now we need to extract the payload, this turned out to be the most difficult task. There are no built-in functions in the kernel to work with the payload, you can only parse the headers of higher-level protocols.

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

What is happening:

  1. I had to include additional header files, this time to manipulate IP and ICMP headers.
  2. I set the maximum string length: #define MAX_CMD_LEN 1976. Why exactly like this? Because the compiler swears at a big one! I have already been told that I need to deal with the stack and the heap, someday I will definitely do this and maybe even correct the code. Immediately I set the line in which the command will lie: char cmd_string[MAX_CMD_LEN];. It should be visible in all functions, I will talk about this in more detail in paragraph 9.
  3. Now we need to initializestruct work_struct my_work;) structure and associate it with another function (DECLARE_WORK(my_work, work_handler);). I will also talk about why this is needed in the ninth paragraph.
  4. Now I declare a function that will be a hook. The type and arguments accepted are dictated by the netfilter, we are only interested in skb. This is the socket buffer, the fundamental data structure that contains all the available information about the packet.
  5. For the function to work, you need two structures, and several variables, including two iterators.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Let's move on to logic. The module does not require any packets other than ICMP Echo, so we parse the buffer with built-in functions and discard all non-ICMP and non-Echo packets. Return NF_ACCEPT means accepting the package, but you can also drop packages by returning 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;
      }

    I haven't tested what happens without checking the IP headers. My minimal knowledge of C tells me that without additional checks, something terrible is bound to happen. I will be glad if you dissuade me from this!

  7. Now that the package is exactly the right type, you can extract the data. Without a built-in function, you first have to get a pointer to the beginning of the payload. This is done through one place, you need to take the pointer to the beginning of the ICMP header and move it to the size of this header. Structure is used for everything. icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    The end of the header must match the end of the payload in skb, so we get it by nuclear means from the corresponding structure: tail = skb_tail_pointer(skb);.

    Nuclear shell over ICMP

    I stole the picture hence, you can read more about the socket buffer.

  8. Having received pointers to the beginning and end, you can copy the data into a string cmd_string, check it for a prefix run: and, either throw out the package if it is missing, or overwrite the line again, removing this prefix.
  9. That's it, now you can call another handler: schedule_work(&my_work);. Since it is impossible to pass a parameter to such a call, the line with the command must be global. schedule_work() will put the function associated with the passed structure into the general task scheduler queue and exit without waiting for the command to complete. This is necessary because the hook must be very fast. Otherwise, you have, to choose from, nothing will start or you will get a kernel panic. Procrastination is like death!
  10. Everything, you can accept the package with the appropriate return.

Calling a program in userspace

This function is the most understandable. Its name was given in DECLARE_WORK(), the type, and the arguments it takes are not interesting. We take the line with the command and pass it entirely to the shell. Let him deal with parsing, searching for binaries and everything else.

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. Set arguments to an array of strings argv[]. I will assume that everyone knows that programs are actually executed in this way, and not in a continuous line with spaces.
  2. Set environment variables. I inserted only PATH with a minimum set of paths, counting on all already merged /bin с /usr/bin и /sbin с /usr/sbin. Other paths rarely matter in practice.
  3. Done, let's do it! Kernel function call_usermodehelper() accepts input. path to binary, array of arguments, array of environment variables. Here I also assume that everyone understands the meaning of passing the path to the executable file as a separate argument, but you can ask. The last argument specifies whether to wait for the process to terminate (UMH_WAIT_PROC), process start (UMH_WAIT_EXEC) or not wait at all (UMH_NO_WAIT). Is there some more UMH_KILLABLE, I didn't look into it.

Assembly

The assembly of kernel modules is done through the kernel make-framework. called make inside a special directory linked to the kernel version (defined here: KERNELDIR:=/lib/modules/$(shell uname -r)/build), and the location of the module is passed to the variable M in arguments. The icmpshell.ko and clean targets use this framework entirely. IN obj-m specifies the object file that will be converted into a module. Syntax that reworks main.o Π² icmpshell.o (icmpshell-objs = main.o) doesn't seem very logical to me, but so be it.

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

We collect: make. Loading: insmod icmpshell.ko. Done, you can check: sudo ./send.py 45.11.26.232 "date > /tmp/test". If you have a file on your machine /tmp/test and it contains the date the request was sent, so you did everything right and I did everything right.

Conclusion

My first experience with nuclear development turned out to be much simpler than I expected. Even without experience in C development, focusing on compiler hints and Google output, I was able to write a working module and feel like a kernel hacker, and at the same time a script kiddy. In addition, I went to the Kernel Newbies channel, where I was told to use schedule_work() instead of calling call_usermodehelper() inside the hook itself and shamed, rightly suspecting a scam. A hundred lines of code cost me about a week of development in my spare time. A successful experience that destroyed my personal myth about the unbearable complexity of system development.

If someone agrees to perform a code review on github, I will be grateful. I'm pretty sure I've made a lot of stupid mistakes, especially with strings.

Nuclear shell over ICMP

Source: habr.com

Add a comment