Analysis of Metasploit linux/x64/exec shellcode

linux/x64/exec utilizes -c flag of system command interpreter (ie. dash on Ubuntu systems) and executes given command in non-login and non-interactive session. Important is that given command is executed as string operand instead being read from stdin.

Consider following shell command

$ sudo echo "foo" >> /etc/passwd
bash: /etc/passwd: Permission denied

The above redirection will not work because sudo is valid just for the echo command. However with -c flag the above command can be executed with sudoer privileges by the following way

$ sudo sh -c 'echo "foo" >> /etc/passwd'

Shellcode demonstration

Create elf64 executable with msfvenom

$ msfvenom -p linux/x64/exec -f elf -a x64 --platform linux CMD=ls -o exec_x64

Executing the binary will run ls command

$ ./exec_x64 
exec_x64  shellcode  shellcode.c

Shellcode overview

Let’s generate the exec shellcode with msfvenom

$ msfvenom -p linux/x64/exec -f c -a x64 --platform linux CMD=ls

and insert it into C testing wrapper

// shellcode.c
// shellcode testing wrapper

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

// msfvenom -p linux/x64/exec -f c -a x64 --platform linux CMD=ls
unsigned char code[] = \
"\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53"
"\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8\x03\x00"
"\x00\x00\x6c\x73\x00\x56\x57\x48\x89\xe6\x0f\x05";

main()
{
        printf("Shellcode Length:  %zd\n", strlen(code));
        int (*CodeFun)() = (int(*)())code;
        CodeFun();
}

and compile it without buffer overflow stack protection and allow executable stack with -z flag which is passed to the linker

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

Now let’s disassemble the shellcode using gdb

$ gdb -q shellcode
(gdb) break *&code
(gdb) r
(gdb) disas
=>  0x0000000000601040 <+0>:     push   0x3b
    0x0000000000601042 <+2>:     pop    rax
    0x0000000000601043 <+3>:     cdq    
    0x0000000000601044 <+4>:     movabs rbx, 0x68732f6e69622f
    0x000000000060104e <+14>:    push   rbx
    0x000000000060104f <+15>:    mov    rdi, rsp
    0x0000000000601052 <+18>:    push   0x632d
    0x0000000000601057 <+23>:    mov    rsi, rsp
    0x000000000060105a <+26>:    push   rdx
    0x000000000060105b <+27>:    call   0x601063 
    0x0000000000601060 <+32>:    ins    BYTE PTR es:[rdi], dx
    0x0000000000601061 <+33>:    jae    0x601063 
    0x0000000000601063 <+35>:    push   rsi
    0x0000000000601064 <+36>:    push   rdi
    0x0000000000601065 <+37>:    mov    rsi, rsp
    0x0000000000601068 <+40>:    syscall

Shellcode analysis

The above stub has just one system call. However the syscall instruction calls execve() system call which is little bit more complicated to setup. I will divide the shellcode with respect to the stack content which contains all arguments for execve() call.

push   0x3b                 
pop    rax                      ; syscall number 59, int execve(const char *filename, char *const argv[], char *const envp[])
cdq                             ; RDX = 0, sign extension zeroing
movabs rbx, 0x68732f6e69622f    ; "hs/nib/"
push   rbx                      ; "hs/nib/" in stack
mov    rdi, rsp                 ; RDI points to "hs/nib/"
;;; stack content, low to high memory ;;;
; 0x2f  0x62    0x69    0x6e    0x2f    0x73    0x68    0x00 <- '/bin/sh' <- RDI
;;; stack content, low to high memory ;;;

RAX is loaded with system call number. “/bin/sh” string is pushed into the stack and RDI is set to point to it.

push   0x632d                   ; "c-"
mov    rsi, rsp                 ; RSI points to "c-"
;;; stack content, low to high memory ;;;
; 0x2d  0x63    0x00    0x00    0x00    0x00    0x00    0x00 '-c'       <- RSI
; 0x2f  0x62    0x69    0x6e    0x2f    0x73    0x68    0x00 '/bin/sh'  <- RDI
;;; stack content, low to high memory ;;;

“-c” string is pushed into the stack and RSI is loaded with its address; exactly the way the RDI was set up.

push   rdx                      ; push 0
call   0x601063                 ; save address of "ls" to stack
ins    BYTE PTR es:[rdi],dx     ; 'l'
jae    0x601063                 ; 's'
;;; stack content, low to high memory ;;;
; 0x60    0x10    0x60    0x00    0x00    0x00    0x00    0x00 address of 'ls'
; 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
; 0x2d    0x63    0x00    0x00    0x00    0x00    0x00    0x00 '-c'       <- RSI
; 0x2f    0x62    0x69    0x6e    0x2f    0x73    0x68    0x00 '/bin/sh'  <- RDI
;;; stack content, low to high memory ;;;

push rdx pushes 8 null bytes into the stack then call instruction pushes address of next instruction (this address effectively points to “ls” string) into the stack. The program flow is redirected to the start of next assembly stub.

push   rsi
push   rdi
;;; stack content, low to high memory ;;;
; 0x60    0xe1    0xff    0xff    0xff    0x7f    0x00    0x00 address of '/bin/sh'
; 0x58    0xe1    0xff    0xff    0xff    0x7f    0x00    0x00 address of '-c'
; 0x60    0x10    0x60    0x00    0x00    0x00    0x00    0x00 address of 'ls'
; 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
; 0x2d    0x63    0x00    0x00    0x00    0x00    0x00    0x00 '-c'       <- RSI
; 0x2f    0x62    0x69    0x6e    0x2f    0x73    0x68    0x00 '/bin/sh'  <- RDI
;;; stack content, low to high memory ;;;

Addresses of strings “-c” and “/bin/sh” are pushed into the stack.

mov    rsi, rsp
;;; stack content, low to high memory ;;;
; 0x60    0xe1    0xff    0xff    0xff    0x7f    0x00    0x00 address of '/bin/sh' <- RSI
; 0x58    0xe1    0xff    0xff    0xff    0x7f    0x00    0x00 address of '-c'
; 0x60    0x10    0x60    0x00    0x00    0x00    0x00    0x00 address of 'ls'
; 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
; 0x2d    0x63    0x00    0x00    0x00    0x00    0x00    0x00 '-c'
; 0x2f    0x62    0x69    0x6e    0x2f    0x73    0x68    0x00 '/bin/sh'  <- RDI
;;; stack content, low to high memory ;;;
syscall                         ; invoke system call execve(RSI, RDI, RDX)

Tha last stub is the most important one because it shows exact configuration of the stack right before the execve() execution. Remind the execve() call signature.

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

RDI represents const char *filename argument and directly points to “/bin/sh” string. RSI represents char *const argv[] argument and contains pointer to pointers! RDX represents char *const envp[] argument and was set to NULL by cdq instruction in the very first assembly stub.

By quickly checking strace output we can clearly see executed execve() call

$ strace ./shellcode 2>&1| grep execve
...
execve("/bin/sh", ["/bin/sh", "-c", "ls"], [/ 0 vars */]) = 0

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

Github code

Student ID: SLAE64 – 1629

Leave a Reply