From 40d61581c369f4a32de7fd9047ea9cd22420b9e9 Mon Sep 17 00:00:00 2001 From: An00bRektn Date: Sun, 28 Aug 2022 16:27:45 -0500 Subject: [PATCH 1/5] Added .gitignore --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..728a650 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Python +.env +venv +env +**/__pycache__ +**/*.pyc +**/*.swp +**/*.egg-info/ +dist/ +build/ +.idea/ \ No newline at end of file From 12579eea81c8dc6865bafd978c0a62c0f3d82b87 Mon Sep 17 00:00:00 2001 From: An00bRektn Date: Sun, 28 Aug 2022 16:58:29 -0500 Subject: [PATCH 2/5] AES-128 in ECB mode implemented --- shellcrypt.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/shellcrypt.py b/shellcrypt.py index 6b98a1f..50cb60c 100644 --- a/shellcrypt.py +++ b/shellcrypt.py @@ -13,6 +13,9 @@ from random import choices from string import hexdigits +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad + # global vars VERSION = "v1.0 beta" OUTPUT_FORMATS = [ @@ -21,6 +24,12 @@ "nim" ] +# Let's just keep it at AES-128 for now +CIPHERS = [ + "xor", + "aes" +] + def show_banner(): # TODO: add support for nocolour maybe? @@ -162,8 +171,30 @@ def generate(self, output_format:str, arrays:dict) -> str: # Pass execution to the respective handler and return return self.__format_handlers[output_format](arrays) - - +class Encrypt: + """Consolidates encryption into a single class""" + def __init__(self, cipher:str, key:bytes): + self.cipher = cipher + self.key = key + + def encrypt(self, plaintext: bytes): + """Encrypts plaintext with the cipher that the object was initialized with""" + if self.cipher == "xor": + return self.__xor(plaintext) + elif self.cipher == "aes": + return self.__aes_128(plaintext) + + def __xor(self, plaintext: bytes): + """Encrypts the input plaintext with a repeating XOR key""" + return bytearray(a ^ b for (a, b) in zip(plaintext, cycle(self.key))) + + # TODO: Support specified IV and other modes. + # Currently just ECB, and other modes are probably stealthier. + def __aes_128(self, plaintext: bytes): + """Encrypts the input plaintext with AES-128 in ECB mode.""" + aes_cipher = AES.new(self.key, AES.MODE_ECB) + plaintext = pad(plaintext, 16) + return aes_cipher.encrypt(plaintext) if __name__ == "__main__": # --------- Initialisation --------- @@ -177,10 +208,11 @@ def generate(self, output_format:str, arrays:dict) -> str: # 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'.") + argparser.add_argument("-e", "--encrypt", default="xor", help="Encryption method to use, default 'xor'.") argparser.add_argument("-k", "--key", help="Encryption key in hex format, default (random 8 bytes).") argparser.add_argument("-f", "--format", help="Output format, specify --formats for a list of formats.") 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("-o", "--output", help="Path to output file") argparser.add_argument("-v", "--version", action="store_true", help="Shows the version and exits") args = argparser.parse_args() @@ -193,6 +225,13 @@ def generate(self, output_format:str, arrays:dict) -> str: print(f" - {i}") exit() + # If ciphers specified + if args.ciphers: + print("The following ciphers are available:") + for i in CIPHERS: + print(f" - {i}") + exit() + # If version specified if args.version: print(VERSION) @@ -222,15 +261,25 @@ def generate(self, output_format:str, arrays:dict) -> str: 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) ") + 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(8) + key = urandom(16) # changed from 8 to 16 to make AES support easier :) else: if len(args.key) < 2 or len(args.key) % 1 == 1: Log.logError(f"Key must be valid byte(s) in hex format (e.g. 4141).") exit() + if args.encrypt == "aes" and len(args.key) != 32: + Log.logError(f"AES-128 key must be exactly 16 bytes long.") + exit() for i in args.key: if i not in hexdigits: Log.logError(f"Key must be valid byte(s) in hex format (e.g. 4141).") @@ -253,7 +302,9 @@ def generate(self, output_format:str, arrays:dict) -> str: #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))) + #input_bytes = bytearray(a ^ b for (a, b) in zip(input_bytes, cycle(key))) + cryptor = Encrypt(cipher=args.encrypt, key=key) + input_bytes = cryptor.encrypt(input_bytes) input_length = len(input_bytes) Log.logSuccess(f"Successfully encrypted input file ({len(input_bytes)} bytes)") From d4dd8d8318968b483afa92454a6a25578d8a73e9 Mon Sep 17 00:00:00 2001 From: An00bRektn Date: Sun, 28 Aug 2022 17:10:43 -0500 Subject: [PATCH 3/5] Updated README and requirements.txt accordingly love 'professionally' amending mistakes ;) --- README.md | 7 +++++-- requirements.txt | Bin 17 -> 80 bytes 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac4ff90..5dc192c 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,19 @@ python ./shellcrypt.py -i ./shellcode.bin -f nim -o ./shellcode_out.nim ~ @0xLegacyy (Jordan Jay) -python ./shellcrypt.py [-h] [-i INPUT] [-k KEY] [-f FORMAT] [--formats] [-o OUTPUT] [-v] +usage: shellcrypt [-h] [-i INPUT] [-e ENCRYPT] [-k KEY] [-f FORMAT] [--formats] [--ciphers] [-o OUTPUT] [-v] -options: +optional arguments: -h, --help show this help message and exit -i INPUT, --input INPUT Path to file to be encrypted. + -e ENCRYPT, --encrypt ENCRYPT + Encryption method to use, default 'xor'. -k KEY, --key KEY Encryption key in hex format, default (random 8 bytes). -f FORMAT, --format FORMAT Output format, specify --formats for a list of formats. --formats Show a list of valid formats + --ciphers Show a list of valid ciphers -o OUTPUT, --output OUTPUT Path to output file -v, --version Shows the version and exits diff --git a/requirements.txt b/requirements.txt index 8c8d131ef6fd1e04ad00508fddafe9d376f46c8a..9154004bf94586d2fdbccaae60ba759fb7eeabd8 100644 GIT binary patch literal 80 zcmezWFPR~qAqR+y7!nzBf!G!Z4H)zoOn}&wftP`cp@5;1AsHxN31pQ3Ri*%OE<-9< Qr7=*IAyAzjP!C8g0H8Pxw*UYD literal 17 YcmYc+N-sz(Do*7}&d Date: Mon, 29 Aug 2022 01:54:54 +0100 Subject: [PATCH 4/5] v1.2 Beta Update - Fixed a bug where all generated output would be stored in an array called `key` (unless you used nim output format) - Code cleanup / standardisation - AES CBC now supported - User-specified IV now supported --- requirements.txt | Bin 80 -> 50 bytes shellcrypt.py | 115 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 79 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9154004bf94586d2fdbccaae60ba759fb7eeabd8..7f5b5f91f7188fbb2fe9ba101af7690ce265941c 100644 GIT binary patch delta 28 icmWGA68!%!nIWGc2Z)Op5*czC5+};bvhXr+F#rI36b8ot delta 58 zcmXpCkpKTLnIWGc2Z)Op5*c!V*cJ#481xuSfY@}RpsWZ;z!=CeWH1Gi1`NCmTnqqy C+6m str: # Generate arrays output = str() for array_name in arrays: - output += f"unsigned char key[{len(arrays[array_name])}] = {{\n" + output += f"unsigned char {array_name}[{len(arrays[array_name])}] = {{\n" output += self.__generate_array_contents(arrays[array_name]) output += "\n};\n\n" @@ -140,7 +140,7 @@ def __output_csharp(self, arrays:dict) -> str: # Generate arrays output = str() for array_name in arrays: - output += f"byte[] key = new byte[{len(arrays[array_name])}] {{\n" + output += f"byte[] {array_name} = new byte[{len(arrays[array_name])}] {{\n" output += self.__generate_array_contents(arrays[array_name]) output += "\n};\n\n" @@ -165,34 +165,51 @@ def generate(self, output_format:str, arrays:dict) -> str: :param shellcode: dictionary containing {"arrayname":array_bytes} pairs :return output: string containing formatted shellcode + key(s) """ - # In future, too many formats to be displayed as choices in argparse - # so look to do some format validation here, and add a --formats argument - # Pass execution to the respective handler and return return self.__format_handlers[output_format](arrays) class Encrypt: - """Consolidates encryption into a single class""" - def __init__(self, cipher:str, key:bytes): - self.cipher = cipher - self.key = key + """ Consolidates encryption into a single class. """ + def __init__(self): + super(Encrypt, self).__init__() + self.__encryption_handlers = { + "xor": self.__xor, + "aes": self.__aes_128 + } + return - def encrypt(self, plaintext: bytes): - """Encrypts plaintext with the cipher that the object was initialized with""" - if self.cipher == "xor": - return self.__xor(plaintext) - elif self.cipher == "aes": - return self.__aes_128(plaintext) + def encrypt(self, cipher:str, plaintext:bytearray, key:bytearray, nonce:bytearray = None) -> 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. + if none will be generated on the fly + :return ciphertext: bytearray containing encrypted plaintext + """ + # If nonce not specified, generate one, otherwise use the specified one. + self.nonce = urandom(16) if nonce is None else nonce + self.key = key + # cipher is already validated (check argument validation section). + return self.__encryption_handlers[cipher](plaintext) - def __xor(self, plaintext: bytes): - """Encrypts the input plaintext with a repeating XOR key""" + def __xor(self, plaintext:bytearray) -> bytearray: + """ Private method to encrypt the input plaintext with a repeating XOR key. + :param plaintext: bytearray containing our plaintext + :return ciphertext: bytearray containing encrypted plaintext + """ return bytearray(a ^ b for (a, b) in zip(plaintext, cycle(self.key))) - # TODO: Support specified IV and other modes. - # Currently just ECB, and other modes are probably stealthier. - def __aes_128(self, plaintext: bytes): - """Encrypts the input plaintext with AES-128 in ECB mode.""" - aes_cipher = AES.new(self.key, AES.MODE_ECB) + # TODO: Support other modes. + # Currently just CBC. + 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 + :return ciphertext: bytearray containing encrypted plaintext + """ + aes_cipher = AES.new(self.key, AES.MODE_CBC, self.nonce) plaintext = pad(plaintext, 16) return aes_cipher.encrypt(plaintext) @@ -209,7 +226,8 @@ def __aes_128(self, plaintext: bytes): 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'.") - argparser.add_argument("-k", "--key", help="Encryption key in hex format, default (random 8 bytes).") + 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).") argparser.add_argument("-f", "--format", help="Output format, specify --formats for a list of formats.") 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") @@ -242,7 +260,7 @@ def __aes_128(self, plaintext: bytes): # Check input file is specified if args.input is None: - Log.logError("Must specify an input file e.g. -i .\shellcode.bin (specify --help for more info)") + Log.logError("Must specify an input file e.g. -i shellcode.bin (specify --help for more info)") exit() # Check input file exists @@ -272,24 +290,43 @@ def __aes_128(self, plaintext: bytes): # if so => validate and store in key # else => generate and store in key if args.key is None: - key = urandom(16) # changed from 8 to 16 to make AES support easier :) + key = urandom(16) # Changed from 8 to 16 to make AES support easier :) else: - if len(args.key) < 2 or len(args.key) % 1 == 1: - Log.logError(f"Key must be valid byte(s) in hex format (e.g. 4141).") + 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).") exit() if args.encrypt == "aes" and len(args.key) != 32: - Log.logError(f"AES-128 key must be exactly 16 bytes long.") + Log.logError("AES-128 key must be exactly 16 bytes long.") exit() for i in args.key: if i not in hexdigits: - Log.logError(f"Key must be valid byte(s) in hex format (e.g. 4141).") + 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: more validation when more args are used + # 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") + 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") @@ -303,8 +340,8 @@ def __aes_128(self, plaintext: bytes): Log.logDebug(f"Encrypting input file") #input_bytes = bytearray(a ^ b for (a, b) in zip(input_bytes, cycle(key))) - cryptor = Encrypt(cipher=args.encrypt, key=key) - input_bytes = cryptor.encrypt(input_bytes) + 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)") @@ -314,10 +351,16 @@ def __aes_128(self, plaintext: bytes): # TODO: have `arrays` dict be generated by the encryption method(s) in use # as only XOR is supported, this is fine for now. arrays = { - "key":key, - "sh3llc0d3":input_bytes + "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) From 5981869c4a04434f314020cdc7d31db7b9f80054 Mon Sep 17 00:00:00 2001 From: Jordan Jay <> Date: Mon, 29 Aug 2022 02:01:34 +0100 Subject: [PATCH 5/5] Update README.md --- README.md | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5dc192c..40391bd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,14 @@ A single-file cross-platform quality of life tool to obfuscate a given shellcode file and output in a useful format for pasting directly into your source code. -![Screenshot of Shellcrypt encrypting shellcode](https://i.imgur.com/Ct6DOg2.png) +![Screenshot of Shellcrypt encrypting shellcode](https://i.imgur.com/ZlIHYu6.png) + +## Encryption Methods + +Shellcrypt currently supports the following encryption methods (more to come in the future!) + +- XOR +- AES (CBC) ## Supported Formats @@ -17,6 +24,10 @@ Shellcrypt currently supports the following output formats (more to come in the ```plaintext python ./shellcrypt.py -i ./shellcode.bin -f c ``` +**Encrypt shellcode with AES CBC** +```plaintext +python ./shellcrypt.py -i ./shellcode.bin -e aes -f c +``` **Encrypt shellcode with a user-specified key** ```plaintext python ./shellcrypt.py -i ./shellcode.bin -f c -k 6d616c77617265 @@ -29,6 +40,14 @@ python ./shellcrypt.py -i ./shellcode.bin -f nim ```plaintext python ./shellcrypt.py -i ./shellcode.bin -f nim -o ./shellcode_out.nim ``` +**Get a list of encryption methods** +```plaintext +python ./shellcrypt.py --ciphers +``` +**Get a list of output formats** +```plaintext +python ./shellcrypt.py --formats +``` **Help** ```plaintext ███████╗██╗ ██╗███████╗██╗ ██╗ ██████╗██████╗ ██╗ ██╗██████╗ ████████╗ @@ -37,19 +56,21 @@ python ./shellcrypt.py -i ./shellcode.bin -f nim -o ./shellcode_out.nim ╚════██║██╔══██║██╔══╝ ██║ ██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ ███████║██║ ██║███████╗███████╗███████╗╚██████╗██║ ██║ ██║ ██║ ██║ ╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ - +v1.2 beta ~ @0xLegacyy (Jordan Jay) -usage: shellcrypt [-h] [-i INPUT] [-e ENCRYPT] [-k KEY] [-f FORMAT] [--formats] [--ciphers] [-o OUTPUT] [-v] +usage: shellcrypt [-h] [-i INPUT] [-e ENCRYPT] [-k KEY] [-n NONCE] [-f FORMAT] [--formats] [--ciphers] [-o OUTPUT] [-v] -optional arguments: +options: -h, --help show this help message and exit -i INPUT, --input INPUT Path to file to be encrypted. -e ENCRYPT, --encrypt ENCRYPT Encryption method to use, default 'xor'. - -k KEY, --key KEY Encryption key in hex format, default (random 8 bytes). + -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). -f FORMAT, --format FORMAT Output format, specify --formats for a list of formats. --formats Show a list of valid formats