Introducción
En esta publicación, profundizamos en el microcódigo de Hex-Rays y exploramos técnicas para manipular el CTree generado para desofuscar y anotar el código descompilado. La sección final incluye un ejemplo práctico que demuestra cómo anotar una tabla de importación personalizada para el análisis de malware.
Esta guía está destinada a ayudar a los ingenieros inversos y a los analistas de malware a comprender mejor las estructuras internas empleadas durante la descompilación de funciones de IDA. Aconsejamos estar atento al SDK de Hex-Rays que se puede encontrar en el directorio de plugins de IDA PRO, todas las estructuras que se analizan a continuación se obtienen de él.
Arquitectura
Hex-Rays descompila una función a través de un proceso de varias etapas que comienza con el código desensamblado de una función:
-
Código de ensamblado a microcódigo:
Realiza una conversión de las instrucciones de ensamblaje que se almacenan en una estructurainsn_t
en instrucciones de microcódigo representadas por una estructuraminsn_t
-
Generación de CTree:
A partir del microcódigo optimizado, Hex-Rays genera el Árbol de Sintaxis Abstracta (AST), sus nodos son declaraciones (cinsn_t
) o expresiones (cexpr_t
); tenga en cuenta que tantocinsn_t
comocexpr_t
heredan de la estructuracitem_t
Microcódigo
El microcódigo es un lenguaje intermedio (IL) empleado por los rayos hexadecimales, generado al levantar el código ensamblador de un binario. Esto tiene múltiples beneficios, una de las cuales es que es independiente del procesador.
En la siguiente captura de pantalla se muestra el ensamblado y el código descompilado, junto con su microcódigo extraído con Lucid, una herramienta que facilita la visualización del microcódigo.
Podemos acceder al MBA (microcode block array) a través de la estructura cfunc_t
de una función descompilada con el campo MBA.
Consejo: obtenemos el cfunc_t
de una función descompilada con el ida_hexrays.decompile
.
mba_t
es una matriz de micro bloques mblock_t
, el primer bloque representa el punto de entrada de la función y el último representa el final. Los micro bloques (mblock_t
) están estructurados en una lista de doble enlace, podemos acceder al bloque siguiente / anterior con campos nextb
/prevb
respectivamente. Cada mblock_t
incluye una lista de instrucciones de microcódigo de doble enlace minsn_t
, a la que se accede mediante el campo head
para la primera instrucción del bloque y tail
para la última instrucción del bloque. La estructura mblock_t
se muestra en el siguiente fragmento de código.
class mblock_t
{
//...
public:
mblock_t *nextb; ///< next block in the doubly linked list
mblock_t *prevb; ///< previous block in the doubly linked list
uint32 flags; ///< combination of \ref MBL_ bits
ea_t start; ///< start address
ea_t end; ///< end address
minsn_t *head; ///< pointer to the first instruction of the block
minsn_t *tail; ///< pointer to the last instruction of the block
mba_t *mba;
Una instrucción de microcódigo minsn_t
es una lista de doble enlace, cada instrucción de microcódigo contiene 3 operandos: izquierda, derecha y destino. Podemos acceder a la instrucción de microcódigo siguiente/anterior del mismo bloque con next
/prev
campos; El campo Opcode es una enumeración (mcode_t
) de todos los códigos de operación de microinstrucciones, por ejemplo, la enumeración m_mov
representa el código de operación mov
.
class minsn_t
{
//...
public:
mcode_t opcode; ///< instruction opcode enumeration
int iprops; ///< combination of \ref IPROP_ bits
minsn_t *next; ///< next insn in doubly linked list. check also nexti()
minsn_t *prev; ///< prev insn in doubly linked list. check also previ()
ea_t ea; ///< instruction address
mop_t l; ///< left operand
mop_t r; ///< right operand
mop_t d; ///< destination operand
//...
enum mcode_t
{
m_nop = 0x00, // nop // no operation
m_stx = 0x01, // stx l, {r=sel, d=off} // store register to memory
m_ldx = 0x02, // ldx {l=sel,r=off}, d // load register from memory
m_ldc = 0x03, // ldc l=const, d // load constant
m_mov = 0x04, // mov l, d // move
m_neg = 0x05, // neg l, d // negate
m_lnot = 0x06, // lnot l, d // logical not
//...
};
Cada operando es de tipo mop_t
, dependiendo del tipo (al que se accede con el campo t
) puede contener registros, valores inmediatos e incluso instrucciones de microcódigo anidadas. A modo de ejemplo, a continuación se muestra el microcódigo de una función con varias instrucciones anidadas:
class mop_t
{
public:
/// Operand type.
mopt_t t;
union
{
mreg_t r; // mop_r register number
mnumber_t *nnn; // mop_n immediate value
minsn_t *d; // mop_d result (destination) of another instruction
stkvar_ref_t *s; // mop_S stack variable
ea_t g; // mop_v global variable (its linear address)
int b; // mop_b block number (used in jmp,call instructions)
mcallinfo_t *f; // mop_f function call information
lvar_ref_t *l; // mop_l local variable
mop_addr_t *a; // mop_a variable whose address is taken
char *helper; // mop_h helper function name
char *cstr; // mop_str utf8 string constant, user representation
mcases_t *c; // mop_c cases
fnumber_t *fpc; // mop_fn floating point constant
mop_pair_t *pair; // mop_p operand pair
scif_t *scif; // mop_sc scattered operand info
};
#...
}
/// Instruction operand types
typedef uint8 mopt_t;
const mopt_t
mop_z = 0, ///< none
mop_r = 1, ///< register (they exist until MMAT_LVARS)
mop_n = 2, ///< immediate number constant
mop_str = 3, ///< immediate string constant (user representation)
#...
La generación de microcódigo avanza a través de varios niveles de madurez, también conocidos como niveles de optimización. El nivel inicial, MMAT_GENERATED
, implica la traducción directa del código ensamblador al microcódigo. El nivel de optimización final antes de generar el CTree es MMAT_LVARS
.
enum mba_maturity_t
{
MMAT_ZERO, ///< microcode does not exist
MMAT_GENERATED, ///< generated microcode
MMAT_PREOPTIMIZED, ///< preoptimized pass is complete
MMAT_LOCOPT, ///< local optimization of each basic block is complete.
///< control flow graph is ready too.
MMAT_CALLS, ///< detected call arguments
MMAT_GLBOPT1, ///< performed the first pass of global optimization
MMAT_GLBOPT2, ///< most global optimization passes are done
MMAT_GLBOPT3, ///< completed all global optimization. microcode is fixed now.
MMAT_LVARS, ///< allocated local variables
};
Ejemplo de recorrido de microcódigo
El siguiente código Python se emplea como ejemplo de cómo recorrer e imprimir las instrucciones de microcódigo de una función, atraviesa el microcódigo generado en el primer nivel de madurez (MMAT_GENERATED
).
import idaapi
import ida_hexrays
import ida_lines
MCODE = sorted([(getattr(ida_hexrays, x), x) for x in filter(lambda y: y.startswith('m_'), dir(ida_hexrays))])
def get_mcode_name(mcode):
"""
Return the name of the given mcode_t.
"""
for value, name in MCODE:
if mcode == value:
return name
return None
def parse_mop_t(mop):
if mop.t != ida_hexrays.mop_z:
return ida_lines.tag_remove(mop._print())
return ''
def parse_minsn_t(minsn):
opcode = get_mcode_name(minsn.opcode)
ea = minsn.ea
text = hex(ea) + " " + opcode
for mop in [minsn.l, minsn.r, minsn.d]:
text += ' ' + parse_mop_t(mop)
print(text)
def parse_mblock_t(mblock):
minsn = mblock.head
while minsn and minsn != mblock.tail:
parse_minsn_t(minsn)
minsn = minsn.next
def parse_mba_t(mba):
for i in range(0, mba.qty):
mblock_n = mba.get_mblock(i)
parse_mblock_t(mblock_n)
def main():
func = idaapi.get_func(here()) # Gets the function at the current cursor
maturity = ida_hexrays.MMAT_GENERATED
mbr = ida_hexrays.mba_ranges_t(func)
hf = ida_hexrays.hexrays_failure_t()
ida_hexrays.mark_cfunc_dirty(func.start_ea)
mba = ida_hexrays.gen_microcode(mbr, hf, None, ida_hexrays.DECOMP_NO_WAIT, maturity)
parse_mba_t(mba)
if __name__ == '__main__':
main()
La salida del script se presenta a continuación: a la izquierda, el microcódigo impreso en la consola, y a la derecha, el código ensamblador de IDA:
CTree
En esta sección, profundizaremos en los elementos principales de la estructura CTree de Hex-Rays, y luego pasaremos a un ejemplo práctico que demuestra cómo anotar una tabla de importación personalizada de malware que carga las API de forma dinámica.
Para una mejor comprensión, aprovecharemos el siguiente complemento (hrdevhelper) que nos permite ver los nodos CTree en IDA como un gráfico.
citem_t
es una clase abstracta que es la base tanto para cinsn_t
como para cexpr_t
, contiene información común como la dirección, el tipo de elemento y la etiqueta, mientras que también presenta constantes como is_expr
, contains_expr
que se pueden usar para conocer el tipo de objeto:
struct citem_t
{
ea_t ea = BADADDR; ///< address that corresponds to the item. may be BADADDR
ctype_t op = cot_empty; ///< item type
int label_num = -1; ///< label number. -1 means no label. items of the expression
///< types (cot_...) should not have labels at the final maturity
///< level, but at the intermediate levels any ctree item
///< may have a label. Labels must be unique. Usually
///< they correspond to the basic block numbers.
mutable int index = -1; ///< an index in cfunc_t::treeitems.
///< meaningful only after print_func()
//...
El tipo de elemento al que se accede con el campo op
indica el tipo de nodo, los nodos de expresión tienen el prefijo cot_
y los nodos de instrucciones tienen el prefijo cit_
, el ejemplo cot_asg
indica que el nodo es una expresión de asignación, mientras que cit_if
indica que el nodo es una instrucción de condición (if).
Dependiendo del tipo de nodo de declaración, un cinsn_t
puede tener un atributo diferente, por ejemplo, si el tipo de elemento es cit_if
podemos acceder al detalle del nodo de condición a través del campo cif
, como se ve en el siguiente fragmento, cinsn_t
se implementa mediante una unión. Tenga en cuenta que una cblock_t
es una declaración en bloque que es una lista de cinsn_t
declaraciones, podemos encontrar este tipo por ejemplo al comienzo de una función o luego de una declaración condicional.
struct cinsn_t : public citem_t
{
union
{
cblock_t *cblock; ///< details of block-statement
cexpr_t *cexpr; ///< details of expression-statement
cif_t *cif; ///< details of if-statement
cfor_t *cfor; ///< details of for-statement
cwhile_t *cwhile; ///< details of while-statement
cdo_t *cdo; ///< details of do-statement
cswitch_t *cswitch; ///< details of switch-statement
creturn_t *creturn; ///< details of return-statement
cgoto_t *cgoto; ///< details of goto-statement
casm_t *casm; ///< details of asm-statement
};
//...
En el siguiente ejemplo, el nodo de condición de tipo cit_if
tiene dos nodos secundarios: el de la izquierda es de tipo cit_block
que representa la rama "True" y el de la derecha es la condición a evaluar, que es una llamada a una función, falta un tercer hijo ya que la condición no tiene una rama "False".
A continuación se muestra un gráfico que muestra el nodo de instrucción cit_if
Encuentre la descompilación asociada para el CTree anterior:
La misma lógica se aplica a las expresiones de los nodos cexpr_t
, dependiendo del tipo de nodo, están disponibles diferentes atributos, por ejemplo, un nodo de tipo cot_asg
tiene nodos hijos accesibles con los campos x
y y
.
struct cexpr_t : public citem_t
{
union
{
cnumber_t *n; ///< used for \ref cot_num
fnumber_t *fpc; ///< used for \ref cot_fnum
struct
{
union
{
var_ref_t v; ///< used for \ref cot_var
ea_t obj_ea; ///< used for \ref cot_obj
};
int refwidth; ///< how many bytes are accessed? (-1: none)
};
struct
{
cexpr_t *x; ///< the first operand of the expression
union
{
cexpr_t *y; ///< the second operand of the expression
carglist_t *a;///< argument list (used for \ref cot_call)
uint32 m; ///< member offset (used for \ref cot_memptr, \ref cot_memref)
///< for unions, the member number
};
union
{
cexpr_t *z; ///< the third operand of the expression
int ptrsize; ///< memory access size (used for \ref cot_ptr, \ref cot_memptr)
};
};
//...
Finalmente, la estructura cfunc_t
contiene información relacionada con la función descompilada, la dirección de la función, la matriz de bloques de microcódigo y el CTree al que se accede con los campos entry_ea
, mba
y body
respectivamente.
struct cfunc_t
{
ea_t entry_ea; ///< function entry address
mba_t *mba; ///< underlying microcode
cinsn_t body; ///< function body, must be a block
//...
Ejemplo de recorrido CTree
El código Python proporcionado sirve como un mini visitante recursivo de un CTree, tenga en cuenta que no maneja todos los tipos de nodos, la última sección describirá cómo usar la clase de visitante incorporada Hex-Rays ctree_visitor_t
. Para empezar, obtenemos el cfunc
de la función mediante ida_hexrays.decompile
y accedemos a su CTree a través del campo body
.
A continuación, comprobamos si el nodo(item) es una expresión o una declaración. Por último, podemos analizar el tipo a través del campo op
y explorar sus nodos secundarios.
import idaapi
import ida_hexrays
OP_TYPE = sorted([(getattr(ida_hexrays, x), x) for x in filter(lambda y: y.startswith('cit_') or y.startswith('cot_'), dir(ida_hexrays))])
def get_op_name(op):
"""
Return the name of the given mcode_t.
"""
for value, name in OP_TYPE:
if op == value:
return name
return None
def explore_ctree(item):
print(f"item address: {hex(item.ea)}, item opname: {item.opname}, item op: {get_op_name(item.op)}")
if item.is_expr():
if item.op == ida_hexrays.cot_asg:
explore_ctree(item.x) # left side
explore_ctree(item.y) # right side
elif item.op == ida_hexrays.cot_call:
explore_ctree(item.x)
for a_item in item.a: # call parameters
explore_ctree(a_item)
elif item.op == ida_hexrays.cot_memptr:
explore_ctree(item.x)
else:
if item.op == ida_hexrays.cit_block:
for i_item in item.cblock: # list of statement nodes
explore_ctree(i_item)
elif item.op == ida_hexrays.cit_expr:
explore_ctree(item.cexpr)
elif item.op == ida_hexrays.cit_return:
explore_ctree(item.creturn.expr)
def main():
cfunc = ida_hexrays.decompile(here())
ctree = cfunc.body
explore_ctree(ctree)
if __name__ == '__main__':
main()
A continuación se muestra la salida del script transversal ejecutado en la start
función de una muestra de BLISTER:
Ejemplo práctico: anotar la tabla de importación personalizada de una muestra de malware
Ahora que obtuvimos información sobre la arquitectura y las estructuras del CTree generado, profundicemos en una aplicación práctica y exploremos cómo automatizar la anotación de una tabla de importación personalizada de malware.
Hex-Rays proporciona una clase de utilidad ctree_visitor_t
que se puede emplear para atravesar y modificar el CTree, dos métodos virtuales importantes que hay que conocer son:
visit_insn
: para visitar un estado de cuentavisit_expr
: para visitar una expresión
Para este ejemplo, se emplea la misma muestra de blíster; luego de ubicar la función que obtiene las direcciones API de Windows por hash en la dirección 0x7FF8CC3B0926 (en el .rsrc sección), agregando la enumeración al IDB y aplicando el tipo de enumeración a su parámetro, creamos una clase que hereda de ctree_visitor_t
, como nos interesan las expresiones, solo visit_expr
anularemos.
La idea es localizar un cot_call
node(1) de la función que resuelve las APIs pasando la dirección obj_ea
del primer nodo hijo a la función idc.get_name
que devolverá el nombre de la función.
if expr.op == idaapi.cot_call:
if idc.get_name(expr.x.obj_ea) == self.func_name:
#...
A continuación, recupere la enumeración del hash accediendo al parámetro derecho del nodo de llamada (2), en nuestro caso el parámetro 3.
carg_1 = expr.a[HASH_ENUM_INDEX]
api_name = ida_lines.tag_remove(carg_1.cexpr.print1(None)) # Get API name
El siguiente paso es localizar la variable a la que se le asignó el valor de dirección de la función WinAPI. Para hacer eso, primero necesitamos ubicar el cot_asg
node(3), padre del nodo de llamada usando el método find_parent_of
bajo cfunc.body
de la función descompilada.
asg_expr = self.cfunc.body.find_parent_of(expr) # Get node parent
Finalmente, podemos acceder al primer nodo hijo (4) bajo el nodo cot_asg
, que es de tipo cot_var
y obtener el nombre de la variable actual, la API de Hex-Rays ida_hexrays.rename_lvar
se emplea para renombrar la nueva variable con el nombre de la API de Windows tomado del parámetro enum.
En última instancia, este proceso puede ahorrar una cantidad significativa de tiempo a un analista. En lugar de dedicar tiempo a volver a etiquetar las variables, pueden dirigir su atención a la funcionalidad principal. La comprensión de cómo funcionan los CTrees puede contribuir al desarrollo de plugins más efectivos, permitiendo el manejo de ofuscaciones más complejas.
Para una comprensión completa y el contexto del ejemplo, encuentre el código completo a continuación:
import idaapi
import ida_hexrays
import idc
import ida_lines
import random
import string
HASH_ENUM_INDEX = 2
def generate_random_string(length):
letters = string.ascii_letters
return "".join(random.choice(letters) for _ in range(length))
class ctree_visitor(ida_hexrays.ctree_visitor_t):
def __init__(self, cfunc):
ida_hexrays.ctree_visitor_t.__init__(self, ida_hexrays.CV_FAST)
self.cfunc = cfunc
self.func_name = "sub_7FF8CC3B0926"# API resolution function name
def visit_expr(self, expr):
if expr.op == idaapi.cot_call:
if idc.get_name(expr.x.obj_ea) == self.func_name:
carg_1 = expr.a[HASH_ENUM_INDEX]
api_name = ida_lines.tag_remove(
carg_1.cexpr.print1(None)
) # Get API name
expr_parent = self.cfunc.body.find_parent_of(expr) # Get node parent
# find asg node
while expr_parent.op != idaapi.cot_asg:
expr_parent = self.cfunc.body.find_parent_of(expr_parent)
if expr_parent.cexpr.x.op == idaapi.cot_var:
lvariable_old_name = (
expr_parent.cexpr.x.v.getv().name
) # get name of variable
ida_hexrays.rename_lvar(
self.cfunc.entry_ea, lvariable_old_name, api_name
) # rename variable
return 0
def main():
cfunc = idaapi.decompile(idc.here())
v = ctree_visitor(cfunc)
v.apply_to(cfunc.body, None)
if __name__ == "__main__":
main()
Conclusión
Al concluir nuestra exploración del microcódigo Hex-Rays y la generación de CTree, obtuvimos técnicas prácticas para navegar por las complejidades de la ofuscación de malware. La capacidad de modificar el pseudocódigo de Hex-Rays nos permite eliminar la ofuscación como la ofuscación de flujo de control, eliminar el código muerto y muchos más. El SDK de C++ de Hex-Rays surge como un recurso valioso, ya que ofrece una guía bien documentada para futuras referencias.
Esperamos que esta guía sea útil para los colegas investigadores y cualquier ávido estudiante, encuentre todos los guiones en nuestro repositorio de investigación.