diff --git a/.gitignore b/.gitignore index 728a650..e5b4072 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ env **/*.egg-info/ dist/ build/ -.idea/ \ No newline at end of file +.idea/ +logs +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 06b0990..26893e1 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,20 @@ A single-file cross-platform quality of life tool to obfuscate a given shellcode These are going here because they deserve it - An00bRektn [github](https://github.com/An00bRektn) [twitter](https://twitter.com/An00bRektn) ♥ - 0xtejas [github](https://github.com/0xtejas) +- Lavender-exe [github](https://github.com/Lavender-exe) ## Encryption Methods Shellcrypt currently supports the following encryption methods (more to come in the future!) -- AES (128-bit CBC) +- AES CBC - 128 +- AES CBC - 256 +- AES ECB - 256 - ChaCha20 - RC4 - Salsa20 - XOR +- XOR with Linear Congruential Generator (LCG) ## Supported Formats @@ -33,51 +37,62 @@ Shellcrypt currently supports the following output formats (more to come in the - Visual Basic for Applications (VBA) - Visual Basic Script (VBS) - Rust +- Javascript +- Zig - Raw ## Usage **Encrypt shellcode with a random key** -```plaintext +```bash python ./shellcrypt.py -i ./shellcode.bin -f c ``` **Encrypt shellcode with 128-bit AES CBC** -```plaintext +```bash python ./shellcrypt.py -i ./shellcode.bin -e aes -f c ``` **Encrypt shellcode with a user-specified key** -```plaintext +```bash python ./shellcrypt.py -i ./shellcode.bin -f c -k 6d616c77617265 ``` **Output in nim format** -```plaintext +```bash python ./shellcrypt.py -i ./shellcode.bin -f nim ``` **Output to file** -```plaintext +```bash python ./shellcrypt.py -i ./shellcode.bin -f nim -o ./shellcode_out.nim ``` +**Get a list of compression methods** +```bash +python ./shellcrypt.py --compressors +``` +**Get a list of encoding methods** +```bash +python ./shellcrypt.py --encoders +``` **Get a list of encryption methods** -```plaintext +```bash python ./shellcrypt.py --ciphers ``` **Get a list of output formats** -```plaintext +```bash python ./shellcrypt.py --formats ``` + **Help** ```plaintext -███████╗██╗ ██╗███████╗██╗ ██╗ ██████╗██████╗ ██╗ ██╗██████╗ ████████╗ -██╔════╝██║ ██║██╔════╝██║ ██║ ██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗╚══██╔══╝ -███████╗███████║█████╗ ██║ ██║ ██║ ██████╔╝ ╚████╔╝ ██████╔╝ ██║ -╚════██║██╔══██║██╔══╝ ██║ ██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ -███████║██║ ██║███████╗███████╗███████╗╚██████╗██║ ██║ ██║ ██║ ██║ -╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ -v1.5 beta + _____ __ ____ __ + / ___// /_ ___ / / /___________ ______ / /_ + \__ \/ __ \/ _ \/ / / ___/ ___/ / / / __ \/ __/ + ___/ / / / / __/ / / /__/ / / /_/ / /_/ / /_ +/____/_/ /_/\___/_/_/\___/_/ \__, / .___/\__/ + /____/_/ +v2.0 - Release - ~ @0xLegacyy (Jordan Jay) +By: @0xLegacyy (Jordan Jay) -usage: shellcrypt [-h] [-i INPUT] [-e ENCRYPT] [-k KEY] [-n NONCE] [-f FORMAT] [--formats] [--ciphers] [-o OUTPUT] - [-v] +usage: shellcrypt [-h] -i INPUT [-e ENCRYPT] [--decrypt] [-d ENCODE] [-c COMPRESS] [-k KEY] [-n NONCE] [-f FORMAT] [--formats] [--ciphers] [--encoders] [--compressors] [-o OUTPUT] [-v] [--preserve-null] + [--key-length KEY_LENGTH] options: -h, --help show this help message and exit @@ -85,6 +100,11 @@ options: Path to file to be encrypted. -e ENCRYPT, --encrypt ENCRYPT Encryption method to use, default 'xor'. + --decrypt Enable decryption functionality (not yet implemented). + -d ENCODE, --encode ENCODE + Encoding method to use, default None. + -c COMPRESS, --compress COMPRESS + Compression method to use. -k KEY, --key KEY Encryption key in hex format, default (random 16 bytes). -n NONCE, --nonce NONCE Encryption nonce in hex format, default (random 16 bytes). @@ -92,18 +112,23 @@ options: Output format, specify --formats for a list of formats. --formats Show a list of valid formats --ciphers Show a list of valid ciphers + --encoders Show a list of valid encoders + --compressors Show a list of valid compressors -o OUTPUT, --output OUTPUT Path to output file -v, --version Shows the version and exits + --preserve-null Avoid XORing null bytes during XOR encryption. + --key-length KEY_LENGTH + Specify the key length in bytes (default is 16). ``` ## Future Development Goals -1. More output formats (rust etc.) -2. More encryption methods -3. Compression methods -4. Create a config system that allows for chaining encryption/encoding/compression methods -5. Flag to add a decrypt method to the generated code -6. [Shikata](https://github.com/EgeBalci/sgn) encoder mayhaps? +- [x] More output formats (rust etc.) +- [x] More encryption methods +- [x] Compression methods +- [ ] Create a config system that allows for chaining encryption/encoding/compression methods +- [ ] Flag to add a decrypt method to the generated code +- [ ] [Shikata](https://github.com/EgeBalci/sgn) encoder mayhaps? _**pssst** this is still heavily in development so if you'd like to contribute, have a go at working on one of the many `TODO`'s in the code :)_ diff --git a/requirements.txt b/requirements.txt index 7f5b5f9..82f5183 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/shellcrypt.py b/shellcrypt.py index 428729e..f30806e 100644 --- a/shellcrypt.py +++ b/shellcrypt.py @@ -1,23 +1,67 @@ # Shellcraft -# A QoL tool to obfuscate shellcode. +# A QoL tool to obfuscate shellcode. # In the future will be able to chain encoding/encryption/compression methods. # ~ @0xLegacyy (Jordan Jay) import argparse -from colorama import Fore, Back, Style -from colorama import init as colorama_init +import argparse +import base64 +import logging +import os +import pyfiglet +import random + +from rich.console import Console +from rich.theme import Theme +from rich.logging import RichHandler from binascii import hexlify from itertools import cycle from os import urandom from os.path import isfile -from random import choices from string import hexdigits from Crypto.Cipher import AES, ARC4, ChaCha20, Salsa20 from Crypto.Util.Padding import pad +from Crypto.Random import get_random_bytes + + +theme = Theme({ + "success" : "spring_green3", + "info" : "cornflower_blue", + "error" : "red", + "exception" : "red", +}) +console = Console(theme=theme, color_system="auto") + +DEBUG = False + +log_path = "logs" +debug_path = "logs/debug_logs.log" +session_path = "logs/session_logs.log" +if not os.path.exists(log_path): + os.mkdir(log_path) + +if not os.path.exists(debug_path): + with open(debug_path, 'w'): pass + +if not os.path.exists(session_path): + with open(session_path, 'w'): pass + +logging.basicConfig( + level="DEBUG", + format="%(message)s", + datefmt="[%X]", + handlers=[ + RichHandler(rich_tracebacks=True), + logging.FileHandler(debug_path, mode='a', encoding="utf-8"), + logging.FileHandler(session_path, mode='w', encoding="utf-8"), + ], +) + +logger = logging.getLogger("rich") + # global vars -VERSION = "v1.5 beta" OUTPUT_FORMATS = [ "c", "csharp", @@ -28,106 +72,95 @@ "vba", "vbscript", "raw", - "rust" + "rust", + "js", + "zig" ] CIPHERS = [ - "aes", # Let's just keep it at AES-128 for now + "aes_128", + "aes_ecb", + "aes_cbc", "chacha20", "rc4", "salsa20", - "xor" + "xor", + "xor_complex" ] +ENCODING = [ + "alpha32", + "ascii85", + "base64", + "words256" +] + +COMPRESSION = [ + "lznt", + "rle", +] + +VERSION = "v2.0 - Release" def show_banner(): - # TODO: add support for nocolour maybe? - banner = f"""{Fore.CYAN} -███████╗██╗ ██╗███████╗██╗ ██╗ ██████╗██████╗ ██╗ ██╗██████╗ ████████╗ -██╔════╝██║ ██║██╔════╝██║ ██║ ██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗╚══██╔══╝ -███████╗███████║█████╗ ██║ ██║ ██║ ██████╔╝ ╚████╔╝ ██████╔╝ ██║ -╚════██║██╔══██║██╔══╝ ██║ ██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ -███████║██║ ██║███████╗███████╗███████╗╚██████╗██║ ██║ ██║ ██║ ██║ -╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ -{Style.RESET_ALL}{VERSION} - - ~ @0xLegacyy (Jordan Jay) -""" - print(banner) - - -class Log(object): - """ Handles all styled terminal output. """ + banner = pyfiglet.figlet_format("Shellcrypt", font="slant", justify="left") + console.print(f"[bold yellow]{banner}{VERSION}\n[/bold yellow]") + console.print("By: @0xLegacyy (Jordan Jay)\n", style="green4") + + +class Log: + """Handles all styled terminal output.""" def __init__(self): - super(Log, self).__init__() - return - - def logSuccess(msg:str): - """ Logs msg to the terminal with a green [+] appended. - Used to show task success. - :param msg: User-specified message to be output - :return: - """ - print(f"{Style.BRIGHT}{Fore.GREEN}[+]{Fore.RESET}{Style.RESET_ALL} {msg}") - return + pass - def logInfo(msg:str): - """ Logs msg to the terminal with a blue [*] appended - Used to show task status / info. - :param msg: User-specified message to be output - :return: - """ - print(f"{Style.BRIGHT}{Fore.BLUE}[*]{Fore.RESET}{Style.RESET_ALL} {msg}") - return + @staticmethod + def logSuccess(msg: str): + """Logs msg to the terminal with a green [+] appended. Used to show task success.""" + return logger.debug(f"[+] {msg}") - def logDebug(msg:str): - """ Logs msg to the terminal with a magenta [debug] appended - Used to show debug info for nerds. - :param msg: User-specified message to be output - :return: - """ + @staticmethod + def logInfo(msg: str): + """Logs msg to the terminal with a blue [*] appended. Used to show task status / info.""" + return logger.info(f"[!] {msg}") + + @staticmethod + def logDebug(msg: str): + """Logs msg to the terminal with a magenta [debug] appended. Used for debug info.""" if DEBUG: - print(f"{Style.BRIGHT}{Fore.MAGENTA}[debug]{Fore.RESET}{Style.RESET_ALL} {msg}") - return + return logger.debug(f"[+] {msg}") - def logError(msg:str): - """ Logs msg to the terminal with a red [!] appended - Used to show error messages. - :param msg: User-specified message to be output - :return: - """ - print(f"{Style.BRIGHT}{Fore.RED}[!]{Fore.RESET}{Style.RESET_ALL} {msg}") - return + @staticmethod + def logError(msg: str): + """Logs msg to the terminal with a red [!] appended. Used for error messages.""" + return logger.error(f"[-] {msg}") + @staticmethod + def LogException(msg: str): + """Logs msg to the terminal with a red [!!] appended. Used to show error messages.""" + return logger.exception(f"[!!] {msg}") -class ShellcodeFormatter(object): - """ Enables for easy output generation in multiple formats. """ + +class ShellcodeFormatter: + """Generates shellcode output in various formats.""" def __init__(self): - super(ShellcodeFormatter, self).__init__() self.__format_handlers = { - "c": self.__output_c, - "csharp": self.__output_csharp, - "nim": self.__output_nim, - "go": self.__output_go, - "py": self.__output_py, - "ps1": self.__output_ps1, - "vba": self.__output_vba, + "c": self.__output_c, + "csharp": self.__output_csharp, + "nim": self.__output_nim, + "go": self.__output_go, + "py": self.__output_py, + "ps1": self.__output_ps1, + "vba": self.__output_vba, "vbscript": self.__output_vbscript, - "raw": self.__output_raw, - "rust": self.__output_rust + "raw": self.__output_raw, + "rust": self.__output_rust, + "js": self.__output_js, + "zig": self.__output_zig } - return - - def __generate_array_contents(self, input_bytes:bytearray, string_format:bool=False) -> str: - """ Takes a byte array, and generates a string in format - 0xaa,0xff,0xab(up to 15), - 0x4f... - :param input_bytes: bytearray - :param string_format: Whether to print in the \xff format or 0xff - :return: string containing formatted array contents - """ - # TODO: Rework this to support more languages than just those that use the 0x format + + def __generate_array_contents(self, input_bytes: bytearray, string_format=False) -> str: + """Generates formatted shellcode from bytearray.""" output = "" if not string_format: for i in range(len(input_bytes) - 1): @@ -135,195 +168,145 @@ def __generate_array_contents(self, input_bytes:bytearray, string_format:bool=Fa output += "\n\t" output += f"0x{input_bytes[i]:0>2x}," output += f"0x{input_bytes[-1]:0>2x}" - return output[1:] # (strip first \n) + return output[1:] else: for i in range(len(input_bytes) - 1): if i % 15 == 0: output += "\n" output += f"\\x{input_bytes[i]:0>2x}" output += f"\\x{input_bytes[-1]:0>2x}" - return output[1:] # (strip first \n) + return output[1:] - - def __output_c(self, arrays:dict) -> str: - """ Private method to output in C format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in c format, similar - to msfvenom's csharp format. - """ - # Generate arrays - output = str() - for array_name in arrays: - output += f"unsigned char {array_name}[{len(arrays[array_name])}] = {{\n" - output += self.__generate_array_contents(arrays[array_name]) - output += "\n};\n\n" - - return output - - def __output_rust(self, arrays:dict) -> str: - """ Private method to output in Rust format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in rust format, similar - to msfvenom's rust format. - """ - # Generate arrays - output = str() - for array_name in arrays: - output += f"let {array_name}: [u8; {len(arrays[array_name])}] = [\n" - output += self.__generate_array_contents(arrays[array_name]) - output += "\n];\n\n" - - return output - - def __output_csharp(self, arrays:dict) -> str: - """ Private method to output in C# format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in C# format - """ - # Generate arrays - output = str() - for array_name in arrays: - output += f"byte[] {array_name} = new byte[{len(arrays[array_name])}] {{\n" - output += self.__generate_array_contents(arrays[array_name]) + def __output_format(self, arrays: dict, template: str, array_format="unsigned char") -> str: + """Generate shellcode in specified format.""" + output = "" + for array_name, array in arrays.items(): + output += f"{array_format} {array_name}[{len(array)}] = {{\n" + output += self.__generate_array_contents(array) output += "\n};\n\n" - return output - def __output_nim(self, arrays:dict) -> str: - """ Private method to output in nim format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in nim format - """ - # Generate arrays - output = str() - for array_name in arrays: - output += f"var {array_name}: array[{len(arrays[array_name])}, byte] = [\n" - output += "\tbyte " + self.__generate_array_contents(arrays[array_name])[1:] + def __output_c(self, arrays: dict) -> str: + return self.__output_format(arrays, "c") + + def __output_rust(self, arrays: dict) -> str: + return self.__output_format(arrays, "rust", array_format="let") + + def __output_csharp(self, arrays: dict) -> str: + return self.__output_format(arrays, "csharp", array_format="byte[]") + + def __output_nim(self, arrays: dict) -> str: + output = "" + for array_name, array in arrays.items(): + output += f"var {array_name}: array[{len(array)}, byte] = [\n" + output += "\tbyte " + self.__generate_array_contents(array)[1:] output += "\n]\n\n" return output - def __output_go(self, arrays:dict) -> str: - """ Private method to output in golang format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in golang format - """ - # Generate arrays - output = str() - for array_name in arrays: - output += f"{array_name} := []byte{{\n" - output += self.__generate_array_contents(arrays[array_name]) - output += "\n};\n\n" - return output + def __output_go(self, arrays: dict) -> str: + return self.__output_format(arrays, "go", array_format="[]byte") - def __output_py(self, arrays:dict) -> str: - """ Private method to output in python format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in python format - """ - # Note: Technically not best to use the triple quotes here but consistency ig - # Generate arrays - output = str() - for array_name in arrays: + def __output_py(self, arrays: dict) -> str: + output = "" + for array_name, array in arrays.items(): output += f"{array_name} = b\"\"\"" - output += self.__generate_array_contents(arrays[array_name], string_format=True) + output += self.__generate_array_contents(array, string_format=True) output += "\"\"\"\n\n" return output - def __output_ps1(self, arrays:dict) -> str: - """ Private method to output in powershell format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in powershell format - """ - # Note: Technically not best to use the triple quotes here but consistency ig - # Generate arrays - output = str() - for array_name in arrays: + def __output_ps1(self, arrays: dict) -> str: + output = "" + for array_name, array in arrays.items(): output += f"[Byte[]] ${array_name} = " - output += self.__generate_array_contents(arrays[array_name])[1:] + output += self.__generate_array_contents(array)[1:] output += "\n\n" return output - def __output_vba(self, arrays:dict) -> str: - """ Private method to output in visual basic application format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in visual basic application format - """ - # Generate arrays - output = str() - # VBA has a maximum line length of 1023 characters, so have to work around that - for array_name in arrays: - # Array name + def __output_vba(self, arrays: dict) -> str: + output = "" + for array_name, array in arrays.items(): output += f"{array_name} = Array(" line_length = len(output) - # Array contents - array_size = len(arrays[array_name]) - for i, x in enumerate(arrays[array_name]): - if i == array_size - 1: - break - # If within 5 bytes, we have enough to write "222,_", which is enough for any value. + for i, x in enumerate(array): if line_length + 5 > 1022: output += "_\n" line_length = 0 output += f"{x}," line_length += len(f"{x},") - # Array end if line_length + 4 > 1023: output += "_\n" output += f"{x})\n\n" return output - def __output_vbscript(self, arrays:dict) -> str: - """ Private method to output in vbscript format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in vbscript format - """ - # does not have short line lengths - # Generate arrays - output = str() - for array_name in arrays: + def __output_vbscript(self, arrays: dict) -> str: + output = "" + for array_name, array in arrays.items(): output += f"{array_name}=" - output += "".join([f"Chr({str(c)})&" for c in arrays[array_name]])[:-1] + output += "".join([f"Chr({str(c)})&" for c in array])[:-1] output += "\n\n" return output - def __output_raw(self, arrays:dict) -> str: - """ Private method to output shellcode in raw format. - :param arrays: dictionary containing array names and their respective bytes - :return output: string containing shellcode in raw format - """ - # Grab shellcode + def __output_js(self, arrays: dict) -> str: + """JavaScript output.""" + output = "" + for array_name, array in arrays.items(): + output += f"const {array_name} = new Uint8Array({len(array)}); \n" + output += f"{array_name}.set([" + output += self.__generate_array_contents(array) + output += "]);\n\n" + return output + + def __output_zig(self, arrays: dict) -> str: + """Zig output.""" + output = "" + for array_name, array in arrays.items(): + output += f"var {array_name} = []u8{{\n" + output += self.__generate_array_contents(array) + output += "\n};\n\n" + return output + + def __output_raw(self, arrays: dict) -> str: return arrays["sh3llc0d3"] - def generate(self, output_format:str, arrays:dict) -> str: - """ Generates output given the current class configuration - :param output_format: Output format to generate e.g. "c" or "csharp" - :param shellcode: dictionary containing {"arrayname":array_bytes} pairs - :return output: string containing formatted shellcode + key(s) - """ - # Pass execution to the respective handler and return - return self.__format_handlers[output_format](arrays) + def generate(self, output_format: str, arrays: dict) -> str: + """Generates the formatted shellcode based on the output format.""" + return self.__format_handlers.get(output_format)(arrays) + class Encrypt: """ Consolidates encryption into a single class. """ def __init__(self): super(Encrypt, self).__init__() self.__encryption_handlers = { - "xor": self.__xor, - "aes": self.__aes_128, - "rc4": self.__rc4, - "chacha20": self.__chacha20, - "salsa20": self.__salsa20 + "xor": self.__xor, + "xor_complex": self.__xor_complex, + "aes_128": self.__aes_128, + "aes_ecb": self.__aes_ecb, + "aes_cbc": self.__aes_cbc, + "rc4": self.__rc4, + "chacha20": self.__chacha20, + "salsa20": self.__salsa20 } return - def encrypt(self, cipher:str, plaintext:bytearray, key:bytearray, nonce:bytearray = None) -> bytearray: + def __random_key(self) -> int: + self.seed = random.randint(0, 2**32 - 1) + + LCG_A = 1664525 # Multiplier + LCG_C = 1013904223 # Increment + LCG_M = 2**32 # Modulus (2^32) + + self.seed = (LCG_A * self.seed + LCG_C) % LCG_M + return self.seed & 0xFF + + def encrypt(self, cipher:str, plaintext:bytearray, key:bytearray, nonce:bytearray) -> bytearray: """ Encrypts plaintext with the user-specified cipher. This has been written this way to support chaining of multiple encryption methods in the future. :param cipher: cipher to use, e.g. 'xor'/'aes' :param plaintext: bytearray containing our plaintext :param key: bytearray containing our encryption key - :param nonce: bytearray containing nonce for aes etc. + :param nonce: bytearray containing nonce for aes etc. if none will be generated on the fly :return ciphertext: bytearray containing encrypted plaintext """ @@ -340,8 +323,17 @@ def __xor(self, plaintext:bytearray) -> bytearray: """ return bytearray(a ^ b for (a, b) in zip(plaintext, cycle(self.key))) - # TODO: Support other modes. - # Currently just CBC. + def __xor_complex(self, plaintext: bytearray) -> bytearray: + """ + XOR Encrypts/Decrypts given shellcode using a Linear Congruential Generator (LCG) + """ + encrypted_shellcode = bytearray() + for byte in plaintext: + random_key = self.__random_key() + encrypted_shellcode.append(byte ^ random_key) + + return encrypted_shellcode + def __aes_128(self, plaintext:bytearray) -> bytearray: """ Private method to encrypt the input plaintext with AES-128 in CBC mode. :param plaintext: bytearray containing plaintext @@ -350,7 +342,7 @@ def __aes_128(self, plaintext:bytearray) -> bytearray: aes_cipher = AES.new(self.key, AES.MODE_CBC, self.nonce) plaintext = pad(plaintext, 16) return bytearray(aes_cipher.encrypt(plaintext)) - + def __rc4(self, plaintext:bytearray) -> bytearray: """ Private method to encrypt the input plaintext via RC4. :param plaintext: bytearray containing plaintext @@ -358,189 +350,434 @@ def __rc4(self, plaintext:bytearray) -> bytearray: """ rc4_cipher = ARC4.new(self.key) return rc4_cipher.encrypt(plaintext) - + def __chacha20(self, plaintext:bytearray) -> bytearray: """ Private method to encrypt the input plaintext via ChaCha20. :param plaintext: bytearray containing plaintext :return ciphertext: bytearray containing encrypted plaintext """ chacha20_cipher = ChaCha20.new(key=self.key) - return chacha20_cipher.encrypt(plaintext) + return chacha20_cipher.encrypt(plaintext) def __salsa20(self, plaintext:bytearray) -> bytearray: """ Private method to encrypt the input plaintext via Salsa20. :param plaintext: bytearray containing plaintext :return ciphertext: bytearray containing encrypted plaintext """ - salsa20_cipher = Salsa20.new(key=key) + salsa20_cipher = Salsa20.new(key=self.key) return salsa20_cipher.encrypt(plaintext) + def __aes_ecb(self, plaintext:bytearray) -> bytearray: + cipher = AES.new(self.key, AES.MODE_ECB) + padding_length = 16 - len(plaintext) % 16 + padded_shellcode = plaintext + bytearray([padding_length] * padding_length) + return cipher.encrypt(padded_shellcode) -if __name__ == "__main__": - # --------- Initialisation --------- - # Debug mode toggle (logging) - DEBUG = False + def __aes_cbc(self, plaintext:bytearray) -> bytearray: + iv = get_random_bytes(16) + cipher = AES.new(self.key, AES.MODE_CBC, iv) + + padding_length = 16 - len(plaintext) % 16 + padded_shellcode = plaintext + bytearray([padding_length] * padding_length) + + encrypted_shellcode = cipher.encrypt(padded_shellcode) + return bytearray(iv) + bytearray(encrypted_shellcode) + + +class Compress: + def __init__(self): + self.__compression_handlers = { + "lznt": self.__lznt_compress, + "rle": self.__rle_compress + } + self.__decompression_handlers = { + "lznt": self.__lznt_decompress, + "rle": self.__rle_decompress + } + + def compress(self, method: str, data: bytearray) -> bytearray: + """Compress data using specified method.""" + return self.__compression_handlers.get(method)(data) + + def decompress(self, method: str, data: bytearray) -> bytearray: + """Decompress data using specified method.""" + return self.__decompression_handlers.get(method)(data) + + def __lznt1_compress(self, data: bytes) -> bytes: + """Self-contained LZNT1 compressor (LZ77-variant).""" + out = bytearray() + pos = 0 + while pos < len(data): + flag_bits = 0 + flag_pos = len(out) # reserve flag byte + out.append(0) + for bit in range(8): + if pos >= len(data): + break + best_len, best_dist = 0, 0 + max_dist = min(pos, 4096) + for dist in range(1, max_dist + 1): + match_len = 0 + while (match_len < min(18, len(data) - pos) and + data[pos - dist + match_len] == data[pos + match_len]): + match_len += 1 + if match_len >= 3 and match_len > best_len: + best_len, best_dist = match_len, dist + if best_len: + # encode match + flag_bits |= (1 << bit) + desc = ((best_len - 3) << 12) | best_dist + out.extend(desc.to_bytes(2, 'little')) + pos += best_len + else: + # literal + out.append(data[pos]); pos += 1 + out[flag_pos] = flag_bits + return bytes(out) + + def __lznt1_decompress(self, src: bytes, out_size: int) -> bytes: + """Self-contained LZNT1 decompressor.""" + out = bytearray() + pos = 0 + while pos < len(src) and len(out) < out_size: + flags = src[pos]; pos += 1 + for b in range(8): + if pos >= len(src): + break + if flags & (1 << b): + out.append(src[pos]); pos += 1 + else: + if pos + 2 > len(src): + break + desc = int.from_bytes(src[pos:pos+2], 'little'); pos += 2 + length = (desc >> 12) + 3 + distance = desc & 0xFFF + if distance == 0 or distance > len(out): + out.append(desc & 0xFF); out.append(desc >> 8) + else: + for _ in range(length): + out.append(out[-distance]) + if len(out) >= out_size: + break + return bytes(out[:out_size]) + + def __lznt_compress(self, data: bytearray) -> bytearray: + """LZNT compression.""" + return bytearray(self.__lznt1_compress(data)) + + def __lznt_decompress(self, data: bytearray) -> bytearray: + """LZNT decompression.""" + return bytearray(self.__lznt1_decompress(data, len(data) * 10)) + + def __rle_compress(self, data: bytearray) -> bytearray: + """Run-Length Encoding (RLE) compression.""" + compressed = bytearray() + index = 0 + while index < len(data): + byte = data[index] + count = 1 + while index + 1 < len(data) and data[index + 1] == byte: + count += 1 + index += 1 + compressed.extend([byte, count]) + index += 1 + return compressed + + def __rle_decompress(self, data: bytearray) -> bytearray: + """Run-Length Encoding (RLE) decompression.""" + decompressed = bytearray() + for i in range(0, len(data), 2): + byte, count = data[i], data[i + 1] + decompressed.extend([byte] * count) + return decompressed + + +class Encode: + def __init__(self): + self.__encoding_handlers = { + "base64": self.__base64_encode, + "ascii85": self.__ascii85_encode, + "alpha32": self.__alpha32_encode, + "words256": self.__words256_encode + } + self.__decoding_handlers = { + "base64": self.__base64_decode, + "ascii85": self.__ascii85_decode, + "alpha32": self.__alpha32_decode, + "words256": self.__words256_decode + } - # Completely unnecessary stuff (unless you're cool) - colorama_init() - show_banner() + def encode(self, encoding: str, data: bytearray) -> bytearray: + """Encode data using specified encoding.""" + handler = self.__encoding_handlers.get(encoding) + if handler: + return handler(data) + raise ValueError(f"Unsupported encoding: {encoding}") + + def decode(self, decoding: str, data: bytearray) -> bytearray: + """Decode data using specified decoding.""" + handler = self.__decoding_handlers.get(decoding) + if handler: + return handler(data) + raise ValueError(f"Unsupported decoding: {decoding}") + + def __base64_encode(self, data: bytearray) -> bytearray: + """Base64 encoding.""" + return bytearray(base64.b64encode(data)) + + def __base64_decode(self, data: bytearray) -> bytearray: + """Base64 decoding.""" + return bytearray(base64.b64decode(data)) + + def __ascii85_encode(self, data: bytearray) -> bytearray: + """ASCII85 encoding.""" + return bytearray(base64.a85encode(data)) + + def __ascii85_decode(self, data: bytearray) -> bytearray: + """ASCII85 decoding.""" + return bytearray(base64.a85decode(data)) + + def __alpha32_encode(self, data: bytearray) -> bytearray: + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!#$%&'()*+,-./:;<=>?@[]^_`{|}~" + encoded = bytearray() + for byte in data: + encoded.extend(alphabet[byte % len(alphabet)].encode()) + return encoded + + def __alpha32_decode(self, data: bytearray) -> bytearray: + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!#$%&'()*+,-./:;<=>?@[]^_`{|}~" + decoded = bytearray() + for char in data: + decoded.append(alphabet.index(chr(char))) + return decoded + + def __words256_encode(self, data: bytearray) -> bytearray: + words = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", + "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", + "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", + "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu"] + encoded = bytearray() + for byte in data: + encoded.extend(words[byte % len(words)].encode() + b" ") + return encoded + + def __words256_decode(self, data: bytearray) -> bytearray: + words = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", + "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", + "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", + "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu"] + decoded = bytearray() + word = b"" + for byte in data: + word += bytes([byte]) + if word.endswith(b" "): # Word boundary (space) + decoded.append(words.index(word.decode().strip())) + word = b"" + return decoded + + +def parse_args(): + # Parse arguments with additional features + # TODO: Add decryption routines in the future - # Parse arguments argparser = argparse.ArgumentParser(prog="shellcrypt") - argparser.add_argument("-i", "--input", help="Path to file to be encrypted.") - argparser.add_argument("-e", "--encrypt", default="xor", help="Encryption method to use, default 'xor'.") + + # Required argument: Input file + argparser.add_argument("-i", "--input", help="Path to file to be encrypted.", required=True) + + # Encryption related options + argparser.add_argument("-e", "--encrypt", default=None, help="Encryption method to use, default None.") + argparser.add_argument("--decrypt", action="store_true", help="Enable decryption functionality (not yet implemented).") + + # Encoding related options + argparser.add_argument("-d", "--encode", default=None, help="Encoding method to use, default None.") + + # Compression related options + argparser.add_argument("-c", "--compress", default=None, help="Compression method to use.") + + # Key and nonce options argparser.add_argument("-k", "--key", help="Encryption key in hex format, default (random 16 bytes).") argparser.add_argument("-n", "--nonce", help="Encryption nonce in hex format, default (random 16 bytes).") + + # Format related options argparser.add_argument("-f", "--format", help="Output format, specify --formats for a list of formats.") + + # Info-related arguments argparser.add_argument("--formats", action="store_true", help="Show a list of valid formats") argparser.add_argument("--ciphers", action="store_true", help="Show a list of valid ciphers") + argparser.add_argument("--encoders", action="store_true", help="Show a list of valid encoders") + argparser.add_argument("--compressors", action="store_true", help="Show a list of valid compressors") + + # Output file and version argparser.add_argument("-o", "--output", help="Path to output file") + argparser.add_argument("-a", "--array", default="sh3llc0d3", help="Array Name, default sh3llc0d3") argparser.add_argument("-v", "--version", action="store_true", help="Shows the version and exits") - # TODO: Add --preserve-null flag for XOR. (Don't XOR null bytes.) - # TODO: Add length param for random key, currently locked at 16 bytes. - # TODO: Maybe add decryption routines? - args = argparser.parse_args() - - # --------- Info-only arguments --------- - # If formats specified - if args.formats: - print("The following formats are available:") - for i in OUTPUT_FORMATS: - print(f" - {i}") - exit() - # If ciphers specified - if args.ciphers: - print("The following ciphers are available:") - for i in CIPHERS: - print(f" - {i}") - exit() + # Additional Features + # Preserve null bytes during XOR encryption + argparser.add_argument("--preserve-null", action="store_true", help="Avoid XORing null bytes during XOR encryption.") + + # Specify key length (if greater than 16) + argparser.add_argument("--key-length", type=int, default=16, help="Specify the key length in bytes (default is 16).") - # If version specified - if args.version: - print(VERSION) + return argparser.parse_args() + +def print_available_options(option_type, options, exit_on_print=True): + print(f"The following {option_type} are available:") + for option in options: + print(f" - {option}") + if exit_on_print: exit() - - # --------- Argument Validation --------- - Log.logDebug("Validating arguments") - # Check input file is specified - if args.input is None: +def validate_input_file(input_file): + if input_file is None: Log.logError("Must specify an input file e.g. -i shellcode.bin (specify --help for more info)") exit() - - # Check input file exists - if not isfile(args.input): - Log.logError(f"Input file '{args.input}' does not exist.") + if not isfile(input_file): + Log.logError(f"Input file '{input_file}' does not exist.") exit() - - # TODO: check we can read the file. + Log.logSuccess(f"Input file: '{input_file}'") - Log.logSuccess(f"Input file: '{args.input}'") +def validate_and_get_key(key, encrypt_type): + if key is None: + return urandom(32) - # Check format is specified - if args.format not in OUTPUT_FORMATS: - Log.logError("Invalid format specified, please specify a valid format e.g. -f c (--formats gives a list of valid formats) ") + if len(key) < 2 or len(key) % 2 == 1 or any(i not in hexdigits for i in key): + Log.logError("Key must be valid byte(s) in hex format (e.g. 4141).") exit() - - Log.logSuccess(f"Output format: {args.format}") - # Check encrypt is specified - if args.encrypt not in CIPHERS: - Log.logError("Invalid cipher specified, please specify a valid cipher e.g. -e xor (--ciphers gives a list of valid ciphers) ") + if encrypt_type == "aes" and len(key) != 32: + Log.logError("AES-128 key must be exactly 16 bytes long.") exit() - - Log.logSuccess(f"Output format: {args.encrypt}") - - # Check if key is specified. - # if so => validate and store in key - # else => generate and store in key - if args.key is None: - key = urandom(32) # Changed from 8 to 16 to make AES support easier :) - else: - if len(args.key) < 2 or len(args.key) % 2 == 1: - Log.logError("Key must be valid byte(s) in hex format (e.g. 4141).") + + return bytearray.fromhex(key) + +def validate_and_get_nonce(nonce): + if nonce is None: + return urandom(16) + + if len(nonce) != 32 or any(i not in hexdigits for i in nonce): + Log.logError("Nonce must be 16 valid bytes in hex format (e.g. 7468697369736d616c6963696f757321)") + exit() + + return bytearray.fromhex(nonce) + +def process_encoding(input_bytes, args, encoder): + if args.encode: + input_bytes = encoder.encode(args.encode, input_bytes) + return input_bytes + +def process_compression(input_bytes, args, compressor): + if args.compress: + input_bytes = compressor.compress(args.compress, input_bytes) + return input_bytes + +def process_encryption(input_bytes, args, cryptor, key, nonce): + if args.encrypt: + input_bytes = cryptor.encrypt(args.encrypt, input_bytes, key, nonce) + return input_bytes + +def main(): + try: + # Show banner and parse arguments + show_banner() + args = parse_args() + + # --------- Info-only arguments --------- + if args.formats: + print_available_options("formats", OUTPUT_FORMATS) + if args.ciphers: + print_available_options("ciphers", CIPHERS) + if args.encoders: + print_available_options("encoders", ENCODING) + if args.compressors: + print_available_options("compressors", COMPRESSION) + if args.version: + print(VERSION) exit() - if args.encrypt == "aes" and len(args.key) != 32: - Log.logError("AES-128 key must be exactly 16 bytes long.") + + # --------- Argument Validation --------- + Log.logDebug(msg="Validating arguments") + + validate_input_file(args.input) + + if args.format not in OUTPUT_FORMATS: + Log.logError("Invalid format specified, please specify a valid format e.g. -f c (--formats gives a list of valid formats)") exit() - for i in args.key: - if i not in hexdigits: - Log.logError("Key must be valid byte(s) in hex format (e.g. 4141).") - exit() - - key = bytearray.fromhex(args.key) - - Log.logSuccess(f"Using key: {hexlify(key).decode()}") - - # TODO: somehow join the above and this as it's a lot of repeated code, - # maybe some kind of method for checking if an input is hex and 16 bytes ? - # Validate the user's nonce if one is specified, else generate one - if args.nonce is None: - nonce = urandom(16) - else: - if len(args.nonce) != 32: - Log.logError("Nonce must be exactly 16 bytes long") + + Log.logSuccess(f"Output format: {args.format}") + + if args.encrypt and args.encrypt not in CIPHERS: + Log.logError("Invalid cipher specified, please specify a valid cipher e.g. -e xor (--ciphers gives a list of valid ciphers)") exit() - for i in args.nonce: - if i not in hexdigits: - Log.logError("Nonce must be 16 valid bytes in hex format (e.g. 7468697369736d616c6963696f757321)") - exit() - - nonce = bytearray.fromhex(args.nonce) - - # Only show nonce if it's used, could be confusing to the user otherwise - # TODO: probably change this in the future to if args.encrypt in requires_nonce => show - if args.encrypt == "aes": - Log.logSuccess(f"Using nonce: {hexlify(nonce).decode()}") - - Log.logDebug("Arguments validated") - - # --------- Read Input File --------- - input_bytes = None - with open(args.input, "rb") as input_handle: - input_bytes = input_handle.read() - - # --------- Input File Encryption --------- - #Log.logInfo(f"Encrypting {len(input_bytes)} bytes") (came up with a better idea, keeping for future reminder) - Log.logDebug(f"Encrypting input file") - - #input_bytes = bytearray(a ^ b for (a, b) in zip(input_bytes, cycle(key))) - cryptor = Encrypt() - input_bytes = cryptor.encrypt(args.encrypt, input_bytes, key, nonce) - input_length = len(input_bytes) - - Log.logSuccess(f"Successfully encrypted input file ({len(input_bytes)} bytes)") - - # --------- Output Generation --------- - # Define array names + content to be formatted - arrays = { - "key":key - } - - # If aes in use, add nonce to the arrays - if args.encrypt == "aes": - arrays["nonce"] = nonce - - # Removed from the initialization line(s) for arrays for nicer output ordering. - arrays["sh3llc0d3"] = input_bytes - - # Generate formatted output. - shellcode_formatter = ShellcodeFormatter() - output = shellcode_formatter.generate(args.format, arrays) - - # --------- Output --------- - # If no output file specified. - if args.output is None: - # We want to decode if it's a bytearray. (for raw mode) - print(output.decode("latin1") if isinstance(output, bytearray) else output) - exit() - - # If output file specified. - Log.logDebug(f"output var type: {type(output)}") - write_mode = ("wb" if isinstance(output, bytearray) else "w") # We want wb if it's a bytearray. (for raw mode) - Log.logDebug(f"write_mode = \"{write_mode}\"") - with open(args.output, write_mode) as file_handle: - file_handle.write(output) - - Log.logSuccess(f"Output written to '{args.output}'") + + if args.encode and args.encode not in ENCODING: + Log.logError("Invalid encoder specified, please specify a valid encoder e.g. -d ascii85 (--encoders gives a list of valid encoders)") + exit() + + if args.compress and args.compress not in COMPRESSION: + Log.logError("Invalid compression specified, please specify a valid compression e.g. -c lznt (--compressors gives a list of valid compressors)") + exit() + + Log.logSuccess(f"Output Compression: {args.compress}") + Log.logSuccess(f"Output Encryption: {args.encrypt}") + Log.logSuccess(f"Output Encoding: {args.encode}") + + key = validate_and_get_key(args.key, args.encrypt) + Log.logSuccess(f"Using key: {hexlify(key).decode()}") + + nonce = validate_and_get_nonce(args.nonce) + if args.encrypt == "aes": + Log.logSuccess(f"Using nonce: {hexlify(nonce).decode()}") + + Log.logDebug("Arguments validated") + + # --------- Read Input File --------- + with open(args.input, "rb") as input_handle: + input_bytes = input_handle.read() + + # --------- Input File Processing --------- + cryptor = Encrypt() + compressor = Compress() + encoder = Encode() + + if args.compress: + logging.info("Compressing Shellcode") + input_bytes = process_compression(input_bytes, args, compressor) + + if args.encrypt: + logging.info("Encrypting Shellcode") + input_bytes = process_encryption(input_bytes, args, cryptor, key, nonce) + + if args.encode: + logging.info("Encoding Shellcode") + input_bytes = process_encoding(input_bytes, args, encoder) + + Log.logSuccess(f"Successfully processed input file ({len(input_bytes)} bytes)") + + # --------- Output Generation --------- + arrays = {"key": key} + if args.encrypt and args.encrypt in ["aes_128", "aes_ecb", "aes_cbc"]: + arrays["nonce"] = nonce + arrays[args.array] = input_bytes + + # Generate formatted output + shellcode_formatter = ShellcodeFormatter() + output = shellcode_formatter.generate(args.format, arrays) + + # --------- Output --------- + if args.output is None: + console.print(output.decode("latin1") if isinstance(output, bytearray) else output) + exit() + + write_mode = "wb" if isinstance(output, bytearray) else "w" + with open(args.output, write_mode) as file_handle: + file_handle.write(output) + + Log.logSuccess(f"Output written to '{args.output}'") + + except Exception as e: + Log.LogException(f"Exception Caught: {e}") + +if __name__ == "__main__": + main()