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
Student ID: SLAE64 – 1629