Assignment N°5 - Shellcode Analyzing / Dissecting

Assignment Goals (SLAE-1530)

  • Take up at least 3 shellcode samples created using Msfpayload for Linux/x86.

  • Use GDB/Ndisasm/Libemu to dissect the functionality of the shellcode.

  • Present your analysis.

Shellcode Candidates

We will use Msfvenom from Metasploit Framework to generate three different payloads for Linux x86-32.

We can easily enumerate payloads for this architecture and operating system using the following command:

local@user:$ msfvenom -l payloads | grep "linux/x86"

We decided to use the three following payloads:

  1. linux/x86/read_file
  2. linux/x86/chmod
  3. linux/x86/exec

Shellcode N°1 - linux/x86/read_file (Ndisasm)

Generate Payload

To generate our shellcode, let's first understand which parameters are expected for this payload.

local@user:$ msfvenom -p linux/x86/read_file -a x86 --platform Linux --list-options

Name  Current Setting  Required  Description
----  ---------------  --------  -----------
FD    1                yes       The file descriptor to write output to
PATH                   yes       The file path to read

By default output file content is appended to File Descriptor 1 (stdout), we will keep it as is.

We need however to specify the file to be read by the payload. We will choose /etc/passwd file. This file can be read by any user.

We can now generate our final payload to local disk as binary file payload1.bin.

local@user:$ msfvenom -p linux/x86/read_file -a x86 --platform Linux PATH=/etc/passwd -f RAW > payload1.bin

Static Analysis (Ndisasm)

Let's use Ndisasm to retrieve as close as possible assembly instructions that compose this payload.

local@user:$ cat payload1.bin | ndisasm -p intel -b32 -

Output Assembly Analysis

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Jump Call Pop Begin.
; #JUMP.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
00000000  EB36              jmp short 0x38 ; Jump to 0x38 offset.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Open File.
; Syscall n°5 (0x5)
; open(const char *filename, int flags, umode_t mode)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
00000002  B805000000        mov eax,0x5    ; syscall_open()
00000007  5B                pop ebx        ; #POP. "filename" parameter points to `/etc/passwd` 
00000008  31C9              xor ecx,ecx    ; "flags" equal to zero.
0000000A  CD80              int 0x80       ; call syscall.
0000000C  89C3              mov ebx,eax    ; save returned value to ebx. returned value is file handle.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Read File.
; Syscall n°3 (0x3)
; read(unsigned int fd, char *buf, size_t count)
; -- REGISTERS --
; ebx = File descriptor obtained via "open" syscall.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
0000000E  B803000000        mov eax,0x3    ; syscall_read()
00000013  89E7              mov edi,esp    ; copy current stack address to edi register.
00000015  89F9              mov ecx,edi    ; copy edi register to ecx register;
                                           ; this is where file content will be placed (buff)
00000017  BA00100000        mov edx,0x1000 ; "count" equal to 4096 bytes. Number of bytes to read
0000001C  CD80              int 0x80       ; call syscall.
0000001E  89C2              mov edx,eax    ; save returned value to ebx. returned value is the number of bytes read.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Write File.
; Syscall n°4 (0x4)
; write(unsigned int fd, const char *buf, size_t count)
; -- REGISTERS --
; ecx = Stack address used to read file content ("buff") via "read" syscall.
; edx = bytes read from "read" syscall.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
00000020  B804000000        mov eax,0x4     ; syscall_write()
00000025  BB01000000        mov ebx,0x1     ; write to file descriptor 0x1 (stdout)
0000002A  CD80              int 0x80        ; call syscall.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Exit Gracefully.
; Syscall n°1 (0x1)
; exit(int error_code)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
0000002C  B801000000        mov eax,0x1     ; syscall_exit()
00000031  BB00000000        mov ebx,0x0     ; error_code equal to zero.
00000036  CD80              int 0x80        ; call syscall.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; #CALL
; Next address 0x3D is pushed on stack by "call" instruction.
; 0x3D contains target file to read (/etc/passwd)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
00000038  E8C5FFFFFF        call 0x2        ; Go to 0x00000002 offset. 

0000003D  2F                das             ; /
0000003E  657463            gs jz 0xa4      ; etc
00000041  2F                das             ; /
00000042  7061              jo 0xa5         ; pa
00000044  7373              jnc 0xb9        ; ss
00000046  7764              ja 0xac         ; wd
00000048  00                db 0x00         ; NULL

Using static analysis was sufficient to understand every payload actions:

  • Use Jump Call Pop technique to get address of target file (/etc/passwd).
  • Open the target file. File handle / descriptor is stored in ebx.
  • Read first 4096 Bytes to stack. Bytes read is stored in edx.
  • Write those 4096 Bytes to file descriptor 0x1.
  • Exit gracefully.

Shellcode N°2 - linux/x86/chmod (GDB)

This time, instead of using Ndisasm to conduct a static analysis we will use GDB to understand what next payload is doing.

Generate Payload

To generate our shellcode, let's first understand which parameters are expected for this payload.

local@user:$ msfvenom -p linux/x86/chmod -a x86 --platform Linux --list-options

Name  Current Setting  Required  Description
----  ---------------  --------  -----------
FILE  /etc/shadow      yes       Filename to chmod
MODE  0666             yes       File mode (octal)

Both required parameters are already set. Since we are going to execute this payload we will set FILE to a harmless value.

We will create a dummy file:

local@user:$ touch /tmp/test

Then we will generate our payload and set output format to C.

local@user:$ msfvenom -p linux/x86/chmod -a x86 --platform Linux -f C FILE=/tmp/test

unsigned char buf[] = 
"\x99\x6a\x0f\x58\x52\xe8\x0a\x00\x00\x00\x2f\x74\x6d\x70\x2f"
"\x74\x65\x73\x74\x00\x5b\x68\xb6\x01\x00\x00\x59\xcd\x80\x6a"
"\x01\x58\xcd\x80";

We will now place this array in our C template for shellcoding.

#include
#include

unsigned char buf[] = 
"\x99\x6a\x0f\x58\x52\xe8\x0a\x00\x00\x00\x2f\x74\x6d\x70\x2f"
"\x74\x65\x73\x74\x00\x5b\x68\xb6\x01\x00\x00\x59\xcd\x80\x6a"
"\x01\x58\xcd\x80";

main()
{
        printf("Shellcode Length:  %d\n", strlen(buf));

        int (*ret)() = (int(*)())buf;

        ret();
}

We can compile our shellcode using the following command:

local@user:$ gcc shellcode.c -o shellcode -z execstack

Analyzing Shellcode with GDB

local@user:$ gdb ./shellcode

First step is to breakpoint to the shellcode itself.

GDB> b *&buf

Breakpoint 1 at 0x2020

We can now run the program

GDB> r

GDB will break when it reaches our shellcode location.

We can see the assembly code of our shellcode using the disassemble command.

GDB> disassemble

=> 0x00402020 <+0>:     cdq    
   0x00402021 <+1>:     push   0xf
   0x00402023 <+3>:     pop    eax
   0x00402024 <+4>:     push   edx 
   0x00402025 <+5>:     call   0x402034 
   0x0040202a <+10>:    das    
   0x0040202b <+11>:    je     0x40209a
   0x0040202d <+13>:    jo     0x40205e
   0x0040202f <+15>:    je     0x402096
   0x00402031 <+17>:    jae    0x4020a7
   0x00402033 <+19>:    add    BYTE PTR [ebx+0x68],bl
   0x00402036 <+22>:    mov    dh,0x1
   0x00402038 <+24>:    add    BYTE PTR [eax],al
   0x0040203a <+26>:    pop    ecx
   0x0040203b <+27>:    int    0x80
   0x0040203d <+29>:    push   0x1
   0x0040203f <+31>:    pop    eax
   0x00402040 <+32>:    int    0x80
   0x00402042 <+34>:    add    BYTE PTR [eax],al

At first glance it seems that some instruction are broken.

Lets also dump registers values to trace future changes.

GDB> info register

eax            0x402020 0x402020
ecx            0x0      0x0
edx            0x40064a 0x40064a
ebx            0x401fd4 0x401fd4
esp            0xbfffefbc       0xbfffefbc
<...snip...>

It is important to monitor registers between each instructions especially when you don't know what a specific instruction is used for.

Let's continue to next instruction.

GDB> n

Our edx register is now equal to zero.

GDB> print/x $edx

It seems that cdq instruction was used to clear the edx register.

GDB> n

GDB> n

GDB> n

Last three instructions was used to respectively place value 0xf to eax register and push a new zero to the top of the stack.

Interestingly, the call instruction attempts to reach an invalid instruction location 0x402034.

Lets add a new breakpoint to this address and continue execution.

GDB>b *0x402034

GDB>c

esp register now points to a new location. This is perfectly normal, when call instruction is called, the next instruction offset is pushed on the top of the stack.

The next instruction after the call was at offset 0x40202a. This offset contains what seems to be our target file name. We can confirm that using following commands:

GDB> x/x $esp

0xbfffefb4:     0x0040202a

GDB> x/s *(char**)$esp or x/s 0x40202a

0x40202a :      "/tmp/test"

This trick was used to place our target file name in memory and retrieve it location using the call instruction. It is a variant of the Jump Call Pop technique but without the Jump.

GDB> n

File name location was placed on ebx register using the pop instruction.

GDB> n

GDB> n

Last two instructions was used to place on ecx the value 0x1b6

We are currently stopped at a syscall call instruction.

=> 0x40203b :   int    0x80

Let's dump our registers

GDB> info register

eax            0xf      0xf
ecx            0x1b6    0x1b6
edx            0x0      0x0
ebx            0x40202a 0x40202a
<...snip...>
  • eax register contains 0xf which is the syscall number of chmod(const char *filename, umode_t mode).
  • ebx register contains 0x40202a which is the address of our file name /etc/test.
  • ecx register contains 0x1b6 which represent the chmod mode 666.

GDB> n

chmod syscall was successfully triggered, we are sure of that because eax register contains 0x00 which means chmod function succeed.

We can verify if our target file permissions have changed:

GDB> !ls -l /tmp/test

-rw-rw-rw- 1 phrozen phrozen 0 Jun  9 07:48 /tmp/test

GDB> n

GDB> n

Last two instructions was used to place on eax the value 0x1

GDB> n

We are currently stopped at a syscall call instruction.

=> 0x402040 :   int    0x80
  • eax register contains 0x1 which is the syscall number of exit(int error_code)

GDB> n

We've now reached the end of our shellcode and gracefully exit our host program.

Using GDB we were able to understand what the actual shellcode was doing, if we had used Ndisasm we would have been confused by the call instruction.

Shellcode N°3 - linux/x86/exec (Libemu)

For our last payload we will use Libemu to analyze what our shellcode is doing.

You can find instructions about how to download and install Libemu on their official website.

In our guest machine, Libemu was installed on /opt/libemu path.

In some Linux distributions you may install Libemu via aptitude:

local@user:$ apt install libemu-dev

Generate Payload

To generate our shellcode, let’s first understand which parameters are expected for this payload.

local@user:$ msfvenom -p linux/x86/exec -a x86 --platform Linux --list-options

Name  Current Setting  Required  Description                                                                                                                                          
----  ---------------  --------  -----------                                                                                                                                          
CMD                    yes       The command string to execute 

Only one parameter is mandatory, the command to execute. We will set /bin/ls as command.

local@user:$ msfvenom -p linux/x86/exec -a x86 --platform Linux CMD=/bin/ls -f raw | /opt/libemu/bin/sctest -vvv -Ss 100000

Analyzing Shellcode with Libemu

After few seconds we should see the following output

This screenshot contains the "registry dump" after each instructions. This is very useful to trace what the shellcode is doing instruction by instruction.

At the end of the analysis, when this is possible, Libemu generate some pseudo-code to have a clear idea of what the shellcode is doing without spending to much time in understanding each assembly instructions.

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

From above pseudo-code, it is clear that Libemu has detected that the shellcode was using execve syscall to execute a new command via the /bin/sh program.

The full command is: /bin/sh -c "/bin/sh -c /bin/ls"

We can even generate a graph of what shellcode is doing in dot format.

local@user:$ msfvenom -p linux/x86/exec -a x86 --platform Linux CMD=/bin/ls -f raw | /opt/libemu/bin/sctest -vvv -Ss 100000 -G exec.dot

We can easily convert the dot file format to any compatible image format using the dot utility.

local@user:$ dot exec.dot -Tpng -o exec.png

This comes very handy when it comes include further detail in our report analysis.

All content on this website is protected by a disclaimer. Please review it before using our site

June 15, 2020, 11:06 a.m. | By Jean-Pierre LESUEUR