|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +import random |
| 5 | +import string |
| 6 | +import re |
| 7 | +from itertools import product |
| 8 | + |
| 9 | + |
| 10 | +END_SYMBOLS = [' ', '\\', '\n', '.', ',', ';', '=', '`', '"', "'", '(', ')', ':', '[', ']', '{', '}', '+'] # no ++ |
| 11 | +END_SYMBOLS_BY_2 = list(product(END_SYMBOLS, END_SYMBOLS)) |
| 12 | + |
| 13 | + |
| 14 | +def random_string(): |
| 15 | + """Generate a random string of fixed length """ |
| 16 | + length = random.randint(5, 30) |
| 17 | + letters = string.ascii_lowercase + string.ascii_uppercase |
| 18 | + return ''.join(random.choice(letters) for i in range(length)) |
| 19 | + |
| 20 | + |
| 21 | +def random_bits(length): |
| 22 | + """Generate a random 1/0 string of fixed length """ |
| 23 | + chars = '01' |
| 24 | + return ''.join(random.choice(chars) for i in range(length)) |
| 25 | + |
| 26 | + |
| 27 | +def randomize_case(input_string): |
| 28 | + """Randomly change the case of each letter of the input_string""" |
| 29 | + rand_bits = random_bits(len(input_string)) |
| 30 | + return ''.join(input_string[i].lower() if rand_bits[i] == '0' else |
| 31 | + input_string[i].upper() |
| 32 | + for i in range(len(input_string))) |
| 33 | + |
| 34 | + |
| 35 | +def delete_comments(input_text): |
| 36 | + """ |
| 37 | + Completely delete all the comments in the input_text. |
| 38 | + Type 1 comments: "<# ... #>". |
| 39 | + Type 2 comments: "# ... \n", except for cases when # is surrounded with " or '. |
| 40 | + :param input_text: a string representing a script to work on. |
| 41 | + :return: an input_text freed from any comments. |
| 42 | + """ |
| 43 | + output = '' |
| 44 | + start_symbols = ['<#', '#'] |
| 45 | + end_symbols = ['#>', '\n'] |
| 46 | + assert len(start_symbols) == len(end_symbols) |
| 47 | + for i in range(len(start_symbols)): |
| 48 | + output = '' |
| 49 | + # 1. initial search |
| 50 | + start_index = input_text.find(start_symbols[i]) |
| 51 | + while start_index >= 0: |
| 52 | + if input_text[:start_index].split('\n')[-1].replace(" ", "") == "": # handling spaces before the comment |
| 53 | + if len(input_text[:start_index].split('\n')[-1]) > 0: |
| 54 | + start_index = start_index - len(input_text[:start_index].split('\n')[-1]) |
| 55 | + # 2. append everything up to start_index to the output |
| 56 | + output = output + input_text[:start_index] |
| 57 | + # 3. then, either: |
| 58 | + if i == 0 or (i == 1 and input_text[start_index - 1] != "`" and |
| 59 | + ((input_text[:start_index].split('\n')[-1].find("'") == -1 or |
| 60 | + input_text[start_index:].split('\n')[0].find("'") == -1) and |
| 61 | + (input_text[:start_index].split('\n')[-1].find('"') == -1 or |
| 62 | + input_text[start_index:].split('\n')[0].find('"') == -1))): |
| 63 | + # 3.1. skip the comment |
| 64 | + end_index = start_index + input_text[start_index:].find(end_symbols[i]) + len(end_symbols[i]) |
| 65 | + else: |
| 66 | + # 3.2. or add the "false" positive '#' to the output |
| 67 | + end_index = start_index + 1 |
| 68 | + output = output + '#' # we need '#' this time |
| 69 | + # 4. cut input_text from the end position |
| 70 | + input_text = input_text[end_index:] |
| 71 | + # 5. loop |
| 72 | + start_index = input_text.find(start_symbols[i]) |
| 73 | + output = output + input_text |
| 74 | + input_text = output |
| 75 | + while output.find('\n\n\n') != -1: |
| 76 | + output = output.replace('\n\n\n', '\n\n') |
| 77 | + return output |
| 78 | + |
| 79 | + |
| 80 | +def rename_variables(input_text): |
| 81 | + """ |
| 82 | + Randomly rename variables in input_text and return the result with a mapping table. |
| 83 | + :param input_text: a string representing a script to work on. |
| 84 | + :return: (string, dict): an input_text with renamed variables, a mapping table |
| 85 | + """ |
| 86 | + start_symbol = '$' |
| 87 | + powershell_auto_vars = ['$NestedPromptLevel', '$PSBoundParameters', '$ExecutionContext', '$ConsoleFileName', |
| 88 | + '$EventSubscriber', '$SourceEventArgs', '$PSDebugContext', '$PsVersionTable', |
| 89 | + '$PSCommandPath', '$LastExitCode', '$MyInvocation', '$PSScriptRoot', '$PSSenderInfo', |
| 90 | + '$PsUICulture', '$SourceArgs', '$StackTrace', '$EventArgs', '$PsCulture', '$Allnodes', |
| 91 | + '$PsCmdlet', '$ForEach', '$Matches', '$Profile', '$ShellID', '$PsHome', '$PSitem', |
| 92 | + '$Sender', '$Error', '$Event', '$False', '$Input', '$Args', '$Home', '$Host', '$NULL', |
| 93 | + '$This', '$True', '$OFS', '$PID', '$Pwd', '$$', '$?', '$^', '$_', '$('] |
| 94 | + |
| 95 | + powershell_system_vars = [] |
| 96 | + powershell_system_vars = ['Advapi32', 'AccessMask', 'AppDomain', 'Architecture', 'AssemblyName', 'Assembly', |
| 97 | + 'BatPath', 'B64Binary', 'BackupPath', 'BindingFlags', 'binPath', 'Bitfield', |
| 98 | + 'CallingConvention', 'Charset', 'CheckAllPermissionsInSet', 'Class', 'Command', |
| 99 | + 'Credential', 'CurrentUser', 'CustomAttributeBuilder', 'DllBytes', 'DllImportAttribute', |
| 100 | + 'DllName', 'DllPath', 'Emit', 'EntryPoint', 'EnumElements', 'ExcludeProgramFiles', 'Env', |
| 101 | + 'ExcludeWindows', 'ExcludeOwned', 'FieldInfo', 'FieldName', 'FieldProp', 'Field', 'File', |
| 102 | + 'Filter', 'Force', 'FunctionDefinitions', 'FunctionName', 'GetServiceHandle', |
| 103 | + 'InteropServices', 'Kernel32', 'Keys', 'KeyName', 'KnownDLLs', 'LiteralPaths', |
| 104 | + 'LocalGroup', 'MarshalAsAttribute', 'MarshalAs', 'Marshal', 'ModuleBuilder', 'ModuleName', |
| 105 | + 'Module', 'Namespace', 'Name', 'NativeCallingConvention', 'NewField', 'Offset', |
| 106 | + 'OpCodes', 'Out', 'Owners', 'PackingSize', 'ParameterTypes', 'PasswordToAdd', 'Password', |
| 107 | + 'Path', 'PermissionSet', 'Permissions', 'Position', 'ProcessName', 'PropertyInfo', |
| 108 | + 'Properties', 'ReadControl', 'ReplaceString', 'Runtime', 'ReturnType', 'SearchString', |
| 109 | + 'ServiceAccessRights', 'ServiceCommand', 'ServiceCommands', 'ServiceDetails', |
| 110 | + 'ServiceName', 'Service', 'SetLastError', 'SID_AND_ATTRIBUTES', 'SidAttributes', |
| 111 | + 'SizeConst', 'StructBuilder', 'System', 'TargetPermissions', 'TargetService', |
| 112 | + 'TOKEN_GROUPS', 'TokenGroups', 'TypeAttributes', 'TypeHash', 'Types', 'Type', |
| 113 | + 'UnmanagedType', 'UserNameToAdd'] |
| 114 | + powershell_auto_system_vars = powershell_auto_vars + ['$' + i for i in powershell_system_vars] |
| 115 | + # before obfuscation |
| 116 | + not_to_replace_dict = {} |
| 117 | + for system_var in powershell_system_vars: |
| 118 | + # rand_str = random_string() |
| 119 | + if '$' + system_var.lower() in input_text.lower(): |
| 120 | + for es in END_SYMBOLS: |
| 121 | + if '$' + system_var.lower() + es in input_text.lower(): |
| 122 | + # extract children if any |
| 123 | + # ensure there are no variables to obfuscate that will coincide with powershell_system_vars children. |
| 124 | + local_found = input_text.lower().find('$' + system_var.lower() + '.') |
| 125 | + if local_found > -1: |
| 126 | + local_found += len('$' + system_var.lower() + '.') |
| 127 | + child = input_text[local_found: |
| 128 | + local_found + min([input_text[local_found:].find(end_symb) |
| 129 | + for end_symb in END_SYMBOLS |
| 130 | + if input_text[local_found:].find(end_symb) != -1])] |
| 131 | + if '$' + child.lower() not in not_to_replace_dict.keys(): |
| 132 | + for end_symbol in END_SYMBOLS_BY_2: |
| 133 | + if '.' + child.lower() + end_symbol[0] in input_text.lower() and \ |
| 134 | + '$' + child.lower() + end_symbol[1] in input_text.lower(): |
| 135 | + if '$' + child.lower() not in not_to_replace_dict.keys(): |
| 136 | + not_to_replace_dict['$' + child.lower()] = None |
| 137 | + break |
| 138 | + # rename |
| 139 | + # re_sv = re.compile(re.escape('$' + system_var.lower() + es), re.IGNORECASE) |
| 140 | + # input_text = re_sv.sub('$' + system_var + '_' + rand_str + es, input_text) |
| 141 | + # OBFUSCATION STARTS |
| 142 | + input_text_raw = input_text |
| 143 | + vars_dict = {} |
| 144 | + output = '' |
| 145 | + # 1. initial search |
| 146 | + start_index = input_text.find(start_symbol) |
| 147 | + start_index_raw = start_index |
| 148 | + end_index = start_index |
| 149 | + while start_index >= 0: |
| 150 | + # 1. append everything up to start_index to the output |
| 151 | + output = output + input_text[:start_index] |
| 152 | + # 2. then, either 2.1, 2.2 or 2.3: |
| 153 | + # " does not cancel $ symbol, ' does. |
| 154 | + # if we're in "here", ' inside "" does not cancel $. |
| 155 | + assert input_text_raw[:start_index_raw][-start_index:] == input_text[:start_index][-start_index:] |
| 156 | + if (input_text_raw[:start_index_raw].count("'") - |
| 157 | + input_text_raw[:start_index_raw].count("`'") - |
| 158 | + input_text_raw[:start_index_raw].count('"\'"')) % 2 != 0 and not\ |
| 159 | + (input_text_raw[:start_index_raw].count('"') - |
| 160 | + input_text_raw[:start_index_raw].count('`"') - |
| 161 | + input_text_raw[:start_index_raw].count("'\"'")) % 2 != 0: |
| 162 | + # we're in 'here' |
| 163 | + # 2.1.1. add for the "false" positive '$' |
| 164 | + end_index = start_index + 1 |
| 165 | + output = output + "$" |
| 166 | + elif input_text[start_index - 1] == "`" or input_text[start_index + 1] in END_SYMBOLS: |
| 167 | + # 2.1.2. add for the "false" positive '$' |
| 168 | + end_index = start_index + 1 |
| 169 | + output = output + "$" |
| 170 | + elif any([(input_text[start_index: start_index + len(exc_var)].lower() == exc_var.lower() and |
| 171 | + input_text[start_index + len(exc_var)] in END_SYMBOLS) for exc_var in powershell_auto_system_vars]): |
| 172 | + for exc_var in powershell_auto_system_vars: |
| 173 | + if input_text[start_index: start_index + len(exc_var)].lower() == exc_var.lower() and \ |
| 174 | + input_text[start_index + len(exc_var)] in END_SYMBOLS: |
| 175 | + # 2.2. add for the "false" positive '$' |
| 176 | + end_index = start_index + len(exc_var) |
| 177 | + output = output + exc_var # randomize_case(exc_var) |
| 178 | + break |
| 179 | + # value's guaranteed by elif condition. |
| 180 | + else: |
| 181 | + # 2.3. or find the ending |
| 182 | + end_index = start_index |
| 183 | + end_index = end_index + min([input_text[end_index:].find(end_symbol) |
| 184 | + for end_symbol in END_SYMBOLS |
| 185 | + if input_text[end_index:].find(end_symbol) != -1]) |
| 186 | + # check if the ending was a false positive due to escape symbols: |
| 187 | + # assert input_text_raw[:start_index_raw][-8:] == input_text[:start_index][-8:] |
| 188 | +# while input_text[end_index] == "`" or \ |
| 189 | +# (input_text_raw[:start_index_raw].count("'") - |
| 190 | +# input_text_raw[:start_index_raw].count("`'") - |
| 191 | +# input_text_raw[:start_index_raw].count('"\'"')) % 2 != 0 and not \ |
| 192 | + #(input_text_raw[:start_index_raw].count('"') - |
| 193 | + #input_text_raw[:start_index_raw].count('`"') - |
| 194 | + #input_text_raw[:start_index_raw].count("'\"'")) % 2 != 0: |
| 195 | + #end_index = end_index + min([input_text[end_index:].find(end_symbol) |
| 196 | + #for end_symbol in END_SYMBOLS |
| 197 | + #if input_text[end_index:].find(end_symbol) != -1]) |
| 198 | + # 3. generate and append a new variable name |
| 199 | + source_var_name = input_text[start_index: end_index] |
| 200 | + |
| 201 | + res_1 = any([(" -" + source_var_name[1:].lower() + es) in input_text_raw.lower() for es in END_SYMBOLS]) |
| 202 | + res_2 = any([("." + source_var_name[1:].lower() + es) in input_text_raw.lower() for es in END_SYMBOLS]) |
| 203 | + res_3 = ("['" + source_var_name[1:].lower() + "']") in input_text_raw.lower() |
| 204 | + res_4 = ('["' + source_var_name[1:].lower() + '"]') in input_text_raw.lower() |
| 205 | + res_5 = ('$' + source_var_name[1:].lower() + ':') in input_text_raw.lower() |
| 206 | + if source_var_name.lower() not in not_to_replace_dict and not (res_1 or res_2 or res_3 or res_4 or res_5): |
| 207 | + if source_var_name.lower() not in vars_dict: |
| 208 | + vars_dict[source_var_name.lower()] = random_string() |
| 209 | + output = output + "$" + vars_dict[source_var_name.lower()] |
| 210 | + else: |
| 211 | + output = output + source_var_name |
| 212 | + # 4. cut input_text from the end position |
| 213 | + input_text = input_text[end_index:] |
| 214 | + offset = end_index - start_index # if var this is len(source_var_name) |
| 215 | + input_text_raw_cut = input_text_raw[start_index_raw + offset:] |
| 216 | + assert input_text_raw_cut[:-3] == input_text[:-3] |
| 217 | + # 5. loop |
| 218 | + start_index = input_text.find(start_symbol) |
| 219 | + start_index_raw = start_index_raw + offset + start_index |
| 220 | + output = output + input_text |
| 221 | + # 6. additional replacement 1 - function parameter names (FUNCTION_NAME -PARAMETER): |
| 222 | + for end_symbol in END_SYMBOLS: |
| 223 | + if end_symbol != '\\': |
| 224 | + for k, v in vars_dict.items(): |
| 225 | + k_ = k[1:] |
| 226 | + re_k = re.compile(re.escape(" -" + k_ + end_symbol), re.IGNORECASE) |
| 227 | + output = re_k.sub(" -" + v + end_symbol, output) |
| 228 | + # 7. additional replacement 2 - attributes (object.attribute): |
| 229 | + for end_symbol in END_SYMBOLS: |
| 230 | + if end_symbol != '\\': |
| 231 | + for k, v in vars_dict.items(): |
| 232 | + k_ = k[1:] |
| 233 | + re_k = re.compile(re.escape("." + k_ + end_symbol), re.IGNORECASE) |
| 234 | + output = re_k.sub("." + v + end_symbol, output) |
| 235 | + # 8. additional replacement 3 - parameter names in quotes (GetField('PARAMETER')): |
| 236 | + for k, v in vars_dict.items(): |
| 237 | + k_ = k[1:] |
| 238 | + re_k = re.compile(re.escape("'" + k_ + "'"), re.IGNORECASE) |
| 239 | + output = re_k.sub("'" + v + "'", output) |
| 240 | + return output, vars_dict |
| 241 | + |
| 242 | + |
| 243 | +def main(input_text): |
| 244 | + v_dict, f_dict = {}, {} |
| 245 | + output1 = delete_comments(input_text) |
| 246 | + output2, v_dict = rename_variables(output1) |
| 247 | + return output1, output2, v_dict, f_dict |
| 248 | + |
| 249 | + |
| 250 | +if __name__ == '__main__': |
| 251 | + old_file = 'PowerUp.ps1 - Source.txt' |
| 252 | + old_file_split = old_file.split('.') |
| 253 | + new_semi_obfs_file = ''.join(old_file_split[:-1]) + ' - semi-obfuscated.' + old_file_split[-1] |
| 254 | + new_obfs_file = ''.join(old_file_split[:-1]) + ' - obfuscated.' + old_file_split[-1] |
| 255 | + with open(old_file, 'r') as fr: |
| 256 | + input_data = fr.read() |
| 257 | + semi_obfs_data, obfs_data, vars_dict, funcs_dict = main(input_data) |
| 258 | + with open(new_semi_obfs_file, 'w') as f: |
| 259 | + f.write(semi_obfs_data) |
| 260 | + with open(new_obfs_file, 'w') as f: |
| 261 | + f.write(obfs_data) |
| 262 | + vd = sorted([' - '.join(i) for i in vars_dict.items()]) |
| 263 | + fd = sorted([' - '.join(i) for i in funcs_dict.items()]) |
| 264 | + mapping = 'Functions: \n' + '\n'.join(fd) + '\n\n\nVariables: \n' + '\n'.join(vd) |
| 265 | + with open(new_obfs_file + '- name mapping.txt', 'w') as f: |
| 266 | + f.write(str(mapping)) |
0 commit comments