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.