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