Shellcode is a set of crafted machine code instructions that tell the vulnerable program to run additional functions and, in most cases, provide access to a system shell or create a reverse command shell.
Once the shellcode is injected into a process and executed by the vulnerable software or program, it modifies the code run flow to update registers and functions of the program to execute the attacker's code.
It is generally written in Assembly language and translated into hexadecimal opcodes (operational codes). Writing unique and custom shellcode helps in evading AV software significantly. But writing a custom shellcode requires excellent knowledge and skill in dealing with Assembly language, which is not an easy task!
In order to craft your own shellcode, a set of skills is required:
To generate our own shellcode, we need to write and extract bytes from the assembler machine code. For this task, we will be using the AttackBox to create a simple shellcode for Linux that writes the string "THM, Rocks!". The following assembly code uses two main functions:
To call those functions, we will use syscalls. A syscall is the way in which a program requests the kernel to do something. In this case, we will request the kernel to write a string to our screen, and the exit the program. Each operating system has a different calling convention regarding syscalls, meaning that to use the write in Linux, you'll probably use a different syscall than the one you'd use on Windows. For 64-bits Linux, you can call the needed functions from the kernel by setting up the following values:
| rax | System Call | rdi | rsi | rdx |
|---|---|---|---|---|
| 0x1 | sys_write | unsigned int fd | const char *buf | size_t count |
| 0x3c | sys_exit | int error_code |
The table above tells us what values we need to set in different processor registers to call the sys_write and sys_exit functions using syscalls. For 64-bits Linux, the rax register is used to indicate the function in the kernel we wish to call. Setting rax to 0x1 makes the kernel execute sys_write, and setting rax to 0x3c will make the kernel execute sys_exit. Each of the two functions require some parameters to work, which can be set through the rdi, rsi and rdx registers. You can find a complete reference of available 64-bits Linux syscalls here.
For sys_write, the first parameter sent through rdi is the file descriptor to write to. The second parameter in rsi is a pointer to the string we want to print, and the third in rdx is the size of the string to print.
For sys_exit, rdi needs to be set to the exit code for the program. We will use the code 0, which means the program exited successfully.
Copy the following code to your AttackBox in a file called thm.asm:
global _start
section .text
_start:
jmp MESSAGE ; 1) let's jump to MESSAGE
GOBACK:
mov rax, 0x1
mov rdi, 0x1
pop rsi ; 3) we are popping into `rsi`; now we have the
; address of "THM, Rocks!\r\n"
mov rdx, 0xd
syscall
mov rax, 0x3c
mov rdi, 0x0
syscall
MESSAGE:
call GOBACK ; 2) we are going back, since we used `call`, that means
; the return address, which is, in this case, the address
; of "THM, Rocks!\r\n", is pushed into the stack.
db "THM, Rocks!", 0dh, 0ah
Let's explain the ASM code a bit more. First, our message string is stored at the end of the .text section. Since we need a pointer to that message to print it, we will jump to the call instruction before the message itself. When call GOBACK is executed, the address of the next instruction after call will be pushed into the stack, which corresponds to where our message is. Note that the 0dh, 0ah at the end of the message is the binary equivalent to a new line (\r\n).
Next, the program starts the GOBACK routine and prepares the required registers for our first sys_write() function.