Si bien analizar un malware escrito en PowerShell no es una tarea muy complicada, uno de los problemas más comunes al momento realizar ingeniería inversa a este lenguaje de programación interpretado, ocurre cuando su código viene ofuscado, técnica la cual tiene como finalidad hacer el código menos legible para evitar su análisis.
A través de distintos métodos como el renombrado de variables y funciones, codificación de strings, entre otras técnicas, permiten dificultar el análisis estático del script.
Es por ello que frente a esta problemática común, he ideado un script en Python el cual tiene como finalidad lidiar con este tipo de técnicas por medio de las siguientes acciones:
- Decodifica strings ASCII y Unicode codificadas en base64.
- Renombra las variables según su entropía y tipo.
- Renombra las funciones.
Demostración
https://github.com/victorgutierrez92/PS1Decoder
PS1Decoder.py
#!/usr/bin/env python import sys import re import math import base64 val_entropy = 3.25 var_new_object = 0 var_generic = 0 var_int = 0 var_float = 0 var_string = 0 var_string_concat = 0 var_empty_string = 0 var_null = 0 var_true = 0 var_false = 0 def is_int(val): if type(val) == int: return True else: try: int(val) return True except: return False def is_float(val): if type(val) == float: return True else: try: float(val) return True except: return False def entropy(string): prob = [ float(string.count(c)) / len(string) for c in dict.fromkeys(list(string)) ] entropy = - sum([ p * math.log(p) / math.log(2.0) for p in prob ]) return entropy def var_rename(var_name, line): global var_generic global var_new_object global var_int global var_float global var_string global var_string_concat global var_empty_string global var_null global var_true global var_false init_var_rslt = re.findall(r'(' + re.escape(var_name) + r'(\s+)?\=((\s+)?(\'|\"|\$)?.+(\;)?))', line) for n_rslt in init_var_rslt: init_var = n_rslt[2].strip('\n; ') # check true if(init_var.lower() == '$true'): var_true = var_true + 1 return('var_true_' + str(var_true)) # check false if(init_var.lower() == '$false'): var_false = var_false + 1 return('var_false' + str(var_false)) # check null if(init_var.lower() == '$null'): var_null = var_null + 1 return('var_null_' + str(var_null)) # check new-object elif(init_var.find('New-Object') != -1 and init_var.find('New-Object') == 0): var_new_object = var_new_object + 1 return('var_new_object_' + str(var_new_object)) # check int elif(init_var.find('\'') == -1 and init_var.find('"') == -1 and init_var.find('$') == -1 and is_int(init_var)): var_int = var_int + 1 return('var_int_' + init_var + '_' + str(var_int)) # check float elif(init_var.find('\'') == -1 and init_var.find('"') == -1 and init_var.find('$') == -1 and is_float(init_var)): var_float = var_float + 1 return('var_float_' + init_var.replace('.', '__') + '_' + str(var_float)) # check empty string elif(init_var == '\'\'' or init_var == '""'): var_empty_string = var_empty_string + 1 return('var_empty_str_' + str(var_empty_string)) # check string elif(init_var[0] == '\'' or init_var[0] == '"') and (init_var[-1] == '\'' or init_var[-1] == '"'): concat_rslt = re.findall(r'(\'|\"|\})(\s+)?\+(\s+)?(\'|\"|\$)', init_var) if concat_rslt: var_string_concat = var_string_concat + 1 return('var_str_concat_' + str(var_string_concat)) else: var_string = var_string + 1 return('var_str_' + init_var.replace('"', '').replace(' ', '_').replace('.', '__').replace('`', '_backslash_').replace('+', 'concat') + '_' + str(var_string)) else: var_generic = var_generic + 1 return('var_' + str(var_generic)) var_generic = var_generic + 1 return('var_' + str(var_generic)) def deobfuscate(input_file): var_func_count = 0 print('[Info] Reading %s' % input_file) with open(input_file, 'r') as fp_all: cnt_all = fp_all.read() fp_all.close() with open(input_file, 'r') as fp: for cnt in enumerate(fp): # base64 decode base64_rslt = re.search(r'(\$\(\[Text.Encoding\]::(ASCII|Unicode)\.GetString\(\[Convert\]::FromBase64String\(\'(.+)\'\)\)\))', cnt[1]) if base64_rslt: if base64_rslt.group(2) == 'ASCII': base64decode_rslt = base64_rslt.group(3).decode('base64') else: base64decode_rslt = base64_rslt.group(3).decode('base64').replace('\x00', '') cnt_all = cnt_all.replace(base64_rslt.group(0), '"' + base64decode_rslt.replace('"', '`"') + '"') # regex function var_func = re.findall(r'((?i)Function\s+(\w+)(\s+)?)', cnt[1]) for n in var_func: # verifica entropia de la funcion encontrada entropy_rslt = entropy(n[1]) if entropy_rslt >= val_entropy: cnt_all = cnt_all.replace(n[1], 'funcion_' + str(var_func_count)) var_func_count = var_func_count + 1 fp.close() for cnt in cnt_all.splitlines(): # regex var var_rslt = re.findall(r'(\$(\{)?(global:|private:)?(\w+)(\})?)', cnt) for n in var_rslt: # verifica entropia de la variable encontrada entropy_rslt = entropy(n[3]) if entropy_rslt >= val_entropy: cnt_all = cnt_all.replace(n[3], var_rename(n[0], cnt)) output_filename = input_file[:-4] + '_decoded.ps1' print('[Info] Generating %s_decoded.ps1 file ...' % input_file[:-4]) with open(output_filename, 'w') as fp: fp.write(cnt_all) fp.close() return def usage(): print('%s script_powershell.ps1' % sys.argv[0]) def main(): print('PS1 Decoder by .:UND3R:.\n') if len(sys.argv) != 2: usage() exit() deobfuscate(sys.argv[1]) print('[Info] %s decoded successfully :}' % sys.argv[1]) if __name__== '__main__': main()
Conclusión
Como conclusión, fue posible desarrollar un script en Python capaz de hacer más legible los scripts ofuscados. Si bien el script presentado en esta entrada no es una solución definitiva, de cierto modo sienta las bases para la elaboración de scripts que faciliten el proceso de ingeniería inversa en lenguajes de programación interpretado.
Agregar un comentario
Debes iniciar sesión para comentar.