Introduction
Dans cette publication, nous nous penchons sur le microcode Hex-Rays et explorons les techniques de manipulation du CTree généré pour désobfusquer et annoter le code décompilé. La dernière section comprend un exemple pratique montrant comment annoter un tableau d'importation personnalisé pour l'analyse des logiciels malveillants.
Ce guide a pour but d'aider les ingénieurs inverses et les analystes de logiciels malveillants à mieux comprendre les structures internes utilisées lors de la décompilation des fonctions d'IDA. Nous vous conseillons de garder un œil sur le SDK Hex-Rays qui se trouve dans le répertoire des plugins d'IDA PRO, toutes les structures discutées ci-dessous en sont issues.
Architecture
Hex-Rays décompile une fonction par un processus en plusieurs étapes, en commençant par le code désassemblé de la fonction :
-
Code d'assemblage en microcode :
Il convertit les instructions d'assemblage stockées dans une structure en instructions de microcode représentées par un code.insn_t
en instructions de microcode représentées par uneminsn_t
structure -
Génération de CTree :
A partir du microcode optimisé, Hex-Rays génère l'arbre syntaxique abstrait (AST), dont les nœuds sont soit des instructions (cinsn_t
) ou des expressions (cexpr_t
) ; notez quecinsn_t
etcexpr_t
héritent tous deux de la structurecitem_t
structure
Microcode
Le microcode est un langage intermédiaire (IL) utilisé par Hex-Rays, généré en levant le code d'assemblage d'un binaire. Cette méthode présente de nombreux avantages, dont celui d'être indépendante du processeur.
La capture d'écran suivante affiche l'assemblage et le code décompilé, ainsi que son microcode extrait à l'aide de Lucid, un outil qui facilite la visualisation du microcode.
Nous pouvons accéder au MBA (microcode block array) via la structure cfunc_t
d'une fonction décompilée avec le champ MBA.
Astuce : on obtient le cfunc_t
d'une fonction décompilée avec le ida_hexrays.decompile
.
mba_t
est un tableau de micro-blocs mblock_t
le premier bloc représente le point d'entrée de la fonction et le dernier représente la fin. Les micro-blocs (mblock_t
) sont structurés dans une liste doublement liée, nous pouvons accéder au bloc suivant / précédent avec les champs nextb
/prevb
respectivement. Chaque site mblock_t
comprend une liste doublement liée d'instructions de microcodes minsn_t
, accessible par le champ head
pour la première instruction du bloc et tail
pour la dernière instruction du bloc. La structure de mblock_t
est décrite dans l'extrait de code suivant.
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;
Une instruction de microcode minsn_t
est une liste doublement liée, chaque instruction de microcode contient 3 opérandes : gauche, droite et destination. Nous pouvons accéder à l'instruction microcode suivante/précédente du même bloc avec les champs next
/prev
; le champ opcode est une énumération (mcode_t
) de tous les opcodes de microinstruction ; par exemple, l'énumération m_mov
représente l'opcode 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
//...
};
Chaque opérande est de type mop_t
Selon le type (auquel on accède par le champ t
), il peut contenir des registres, des valeurs immédiates et même des instructions de microcode imbriquées. A titre d'exemple, voici le microcode d'une fonction comportant plusieurs instructions imbriquées :
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 génération du microcode passe par différents niveaux de maturité, également appelés niveaux d'optimisation. Le premier niveau, MMAT_GENERATED
, implique la traduction directe du code d'assemblage en microcode. Le niveau d'optimisation final avant la génération du CTree est 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
};
Exemple de traversée du microcode
Le code Python suivant est utilisé comme exemple pour parcourir et imprimer les instructions du microcode d'une fonction, il parcourt le microcode généré au premier niveau de maturité (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 sortie du script est présentée ci-dessous : à gauche, le microcode imprimé dans la console, et à droite, le code d'assemblage par IDA :
CTree
Dans cette section, nous allons nous plonger dans les éléments fondamentaux de la structure CTree de Hex-Rays, puis nous passerons à un exemple pratique démontrant comment annoter une table d'importation personnalisée d'un logiciel malveillant qui charge des API de manière dynamique.
Pour une meilleure compréhension, nous utiliserons le plugin suivant(hrdevhelper) qui nous permet de visualiser les nœuds CTree dans IDA sous la forme d'un graphe.
citem_t
est une classe abstraite qui sert de base aux classes cinsn_t
et cexpr_t
Elle contient des informations communes telles que l'adresse, le type d'objet et l'étiquette, ainsi que des constantes telles que is_expr
, contains_expr
qui peuvent être utilisées pour connaître le type de l'objet :
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()
//...
Le type d'élément auquel on accède avec le champ op
indique le type du nœud, les nœuds d'expression sont préfixés par cot_
et les nœuds d'énoncés sont préfixés par cit_
, par exemple cot_asg
indique que le nœud est une expression d'affectation tandis que cit_if
indique que le nœud est un énoncé de condition (if).
En fonction du type de nœud de déclaration, cinsn_t
peut avoir un attribut différent. Par exemple, si le type d'élément est cit_if
, nous pouvons accéder aux détails du nœud de condition par le biais du champ cif
, comme le montre l'extrait ci-dessous, cinsn_t
est mis en œuvre à l'aide d'une union. Notez qu'un cblock_t
est un bloc d'instructions qui est une liste d'instructions cinsn_t
, on peut trouver ce type d'instructions par exemple au début d'une fonction ou après une instruction conditionnelle.
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
};
//...
Dans l'exemple ci-dessous, le nœud de condition de type cit_if
a deux nœuds enfants : le nœud de gauche est de type cit_block
et représente la branche "True" et le nœud de droite est la condition à évaluer, qui est un appel à une fonction. Il manque un troisième enfant car la condition n'a pas de branche "False".
Le graphique suivant illustre le nœud de l'instruction cit_if
Trouvez la décompilation associée pour le CTree ci-dessus :
La même logique s'applique aux nœuds d'expression cexpr_t
, en fonction du type de nœud, différents attributs sont disponibles. Par exemple, un nœud de type cot_asg
a des nœuds enfants accessibles avec les champs x
et 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)
};
};
//...
Enfin, la structure cfunc_t
contient des informations relatives à la fonction décompilée, à l'adresse de la fonction, au tableau de blocs de microcodes et à l'arbre CT auquel on accède respectivement par les champs entry_ea
, mba
et body
.
struct cfunc_t
{
ea_t entry_ea; ///< function entry address
mba_t *mba; ///< underlying microcode
cinsn_t body; ///< function body, must be a block
//...
Exemple de traversée d'un arbre
Le code Python fourni sert de mini-visiteur récursif d'un CTree, notez qu'il ne gère pas tous les types de noeuds, la dernière section décrira comment utiliser la classe de visiteur intégrée de Hex-Rays ctree_visitor_t
. Pour commencer, nous obtenons le site cfunc
de la fonction à l'aide de ida_hexrays.decompile
et nous accédons à son CTree via le champ body
.
Ensuite, nous vérifions si le nœud (item) est une expression ou une déclaration. Enfin, nous pouvons analyser le type à travers le champ op
et explorer ses nœuds enfants.
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()
Vous trouverez ci-dessous la sortie du script de recherche exécuté sur la start
fonction d'un échantillon BLISTER:
Exemple pratique : annotation de la table d'importation personnalisée d'un échantillon de logiciel malveillant
Maintenant que nous avons mieux compris l'architecture et les structures de l'arbre CT généré, nous allons nous plonger dans une application pratique et explorer comment automatiser l'annotation d'une table d'importation personnalisée de logiciels malveillants.
Hex-Rays fournit une classe utilitaire ctree_visitor_t
qui peut être utilisée pour parcourir et modifier l'arbre CT. Deux méthodes virtuelles importantes sont à connaître :
visit_insn
: pour visiter une déclarationvisit_expr
: pour visiter une expression
Pour cet exemple, le même échantillon BLISTER est utilisé ; après avoir localisé la fonction qui obtient les adresses des API Windows par hachage à l'adresse 0x7FF8CC3B0926 (dans le fichier .rsrc ), en ajoutant l'énumération à la BDI et en appliquant le type d'énumération à son paramètre, nous créons une classe qui hérite de ctree_visitor_t
, puisque nous nous intéressons aux expressions, nous ne remplacerons que visit_expr
.
L'idée est de localiser un nœud cot_call
(1) de la fonction qui résout les API en passant l'adresse obj_ea
du premier enfant du nœud à la fonction idc.get_name
qui renverra le nom de la fonction.
if expr.op == idaapi.cot_call:
if idc.get_name(expr.x.obj_ea) == self.func_name:
#...
Récupérez ensuite l'énumération du hachage en accédant au paramètre droit de l'appel node(2), dans notre cas le paramètre 3.
carg_1 = expr.a[HASH_ENUM_INDEX]
api_name = ida_lines.tag_remove(carg_1.cexpr.print1(None)) # Get API name
L'étape suivante consiste à localiser la variable à laquelle a été attribuée la valeur d'adresse de la fonction WinAPI. Pour ce faire, nous devons d'abord localiser le nœud cot_asg
(3), parent du nœud d'appel, en utilisant la méthode find_parent_of
sous cfunc.body
de la fonction décompilée.
asg_expr = self.cfunc.body.find_parent_of(expr) # Get node parent
Enfin, nous pouvons accéder au premier nœud enfant (4) sous le nœud cot_asg
, qui est de type cot_var
et obtenir le nom de la variable actuelle, l'API Hex-Rays ida_hexrays.rename_lvar
Hex-Rays est utilisée pour renommer la nouvelle variable avec le nom de l'API Windows tiré du paramètre enum.
Ce processus peut en fin de compte faire gagner beaucoup de temps à l'analyste. Au lieu de perdre du temps à renommer des variables, ils peuvent se concentrer sur la fonctionnalité principale. La compréhension du fonctionnement des CTrees peut contribuer au développement de plugins plus efficaces, permettant de traiter des obscurcissements plus complexes.
Pour une compréhension et un contexte complets de l'exemple, veuillez trouver l'intégralité du code ci-dessous :
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()
Conclusion
Pour conclure notre exploration du microcode Hex-Rays et de la génération de CTree, nous avons acquis des techniques pratiques pour naviguer dans les complexités de l'obscurcissement des logiciels malveillants. La capacité de modifier le pseudo-code Hex-Rays nous permet de couper à travers l'obfuscation comme l'obfuscation du flux de contrôle, de supprimer le code mort, et bien d'autres choses encore. Le SDK C++ de Hex-Rays s'avère être une ressource précieuse, offrant des conseils bien documentés pour toute référence future.
Nous espérons que ce guide sera utile à nos collègues chercheurs et à tout apprenant avide, veuillez trouver tous les scripts dans notre référentiel de recherche.