Einführung
In dieser Veröffentlichung befassen wir uns mit dem Hex-Rays-Mikrocode und untersuchen Techniken zur Manipulation des generierten CTree, um dekompilierten Code zu entschleiern und zu kommentieren. Der letzte Abschnitt enthält ein praktisches Beispiel, das zeigt, wie eine benutzerdefinierte Importtabelle für die Malware-Analyse mit Anmerkungen versehen wird.
Dieser Leitfaden soll Reverse Engineers und Malware-Analysten dabei helfen, die internen Strukturen, die während der Funktionsdekompilierung von IDA verwendet werden, besser zu verstehen. Wir empfehlen, ein Auge auf das Hex-Rays SDK zu werfen, das im Plugins-Verzeichnis von IDA PRO zu finden ist, alle unten besprochenen Strukturen stammen von ihm.
Architektur
Hex-Rays dekompiliert eine Funktion durch einen mehrstufigen Prozess, der mit dem disassemblierten Code einer Funktion beginnt:
-
Assembler-Code in Mikrocode:
Es führt eine Konvertierung der Assembleranweisungen, die in einerinsn_t
Struktur gespeichert sind, in Mikrocode-Anweisungen durch, die durch eineminsn_t
Struktur dargestellt werden -
CTree-Generierung:
Aus dem optimierten Mikrocode generiert Hex-Rays den Abstract Syntax Tree (AST), dessen Knoten entweder Anweisungen (cinsn_t
) oder Ausdrücke (cexpr_t
) sind; beachten Sie, dass sowohlcinsn_t
als auchcexpr_t
von dercitem_t
Struktur erben
Microcode
Microcode ist eine Zwischensprache (IL), die von Hex-Rays verwendet wird und durch Anheben des Assembler-Codes einer Binärdatei generiert wird. Dies hat mehrere Vorteile, von denen einer darin besteht, dass es prozessorunabhängig ist.
Der folgende Screenshot zeigt die Assembly und den dekompilierten Code zusammen mit dem Mikrocode, der mit Lucid extrahiert wurde, einem Tool, das die Mikrocode-Visualisierung erleichtert.
Wir können auf das MBA (Microcode Block Array) über die cfunc_t
Struktur einer dekompilierten Funktion mit dem MBA-Feld zugreifen.
Tipp: Den cfunc_t
einer dekompilierten Funktion erhalten wir mit dem ida_hexrays.decompile
.
mba_t
handelt sich um ein Array von Mikroblöcken mblock_t
, stellt der erste Block den Einstiegspunkt der Funktion und der letzte das Ende dar. Mikroblöcke (mblock_t
) sind in einer doppelt verknüpften Liste strukturiert, wir können auf den nächsten / vorherigen Block mit jeweils nextb
/prevb
Feldern zugreifen. Jedes mblock_t
enthält eine doppelt verknüpfte Liste von Mikrocode-Anweisungen minsn_t
, auf die das Feld head
für die erste Anweisung des Blocks und tail
für die letzte Anweisung des Blocks zugreift. Die mblock_t
Struktur wird im folgenden Codeausschnitt dargestellt.
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;
Eine Mikrocodeanweisung minsn_t
es sich um eine doppelt verknüpfte Liste handelt, enthält jede Mikrocodeanweisung 3 Operanden: left, right und destination. Wir können auf die nächste/vorherige Microcode-Anweisung desselben Blocks mit next
/prev
Feldern zugreifen. Das Opcode-Feld ist eine Aufzählung (mcode_t
) aller Microinstruction-Opcodes, z. B. stellt die m_mov
-Enumeration den mov
Opcode dar.
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
//...
};
Jeder Operand ist vom Typ mop_t
, je nach Typ (auf den mit dem Feld t
zugegriffen wird) kann er Register, unmittelbare Werte und sogar verschachtelte Mikrocodeanweisungen enthalten. Als Beispiel sehen Sie im Folgenden den Mikrocode einer Funktion mit mehreren verschachtelten Anweisungen:
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)
#...
Die Microcode-Generierung durchläuft verschiedene Reifegrade, die auch als Optimierungsstufen bezeichnet werden. Die erste Ebene umfasst MMAT_GENERATED
die direkte Übersetzung von Assemblercode in Mikrocode. Die letzte Optimierungsstufe vor der Generierung des CTree ist 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
};
Beispiel für Microcode-Traversal
Der folgende Python-Code wird als Beispiel für das Durchlaufen und Drucken der Mikrocode-Anweisungen einer Funktion verwendet, er durchläuft den Mikrocode, der auf der ersten Reifestufe (MMAT_GENERATED
) generiert wurde.
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()
Die Ausgabe des Skripts wird wie folgt dargestellt: links der gedruckte Mikrocode in der Konsole und rechts der Assembler-Code von IDA:
CTree
In diesem Abschnitt tauchen wir in die Kernelemente der CTree-Struktur von Hex-Rays ein und fahren dann mit einem praktischen Beispiel fort, das zeigt, wie eine benutzerdefinierte Importtabelle von Malware kommentiert wird, die APIs dynamisch lädt.
Zum besseren Verständnis werden wir das folgende Plugin (hrdevhelper) nutzen, das es uns ermöglicht, die CTree-Knoten in IDA als Diagramm anzuzeigen.
citem_t
ist eine abstrakte Klasse, die die Basis für cinsn_t
und cexpr_t
ist, sie enthält allgemeine Informationen wie die Adresse, den Elementtyp und die Beschriftung, während sie auch Konstanten wie is_expr
enthält, contains_expr
, die verwendet werden können, um den Typ des Objekts zu kennen:
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()
//...
Der Elementtyp, auf den über das Feld op
zugegriffen wird, gibt den Typ des Knotens an, Ausdrucksknoten erhalten das Präfix cot_
und den Anweisungsknoten das Präfix cit_
, Beispiel cot_asg
gibt an, dass es sich bei dem Knoten um einen Zuweisungsausdruck handelt, während cit_if
angibt, dass es sich bei dem Knoten um eine Bedingungsanweisung (wenn) handelt.
Abhängig vom Typ des Anweisungsknotens kann ein cinsn_t
ein anderes Attribut haben, z. B. wenn der Elementtyp cit_if
ist, können wir über das Feld cif
auf die Details des Bedingungsknotens zugreifen, wie im folgenden Snippet zu sehen cinsn_t
wird mit einer Union implementiert. Beachten Sie, dass eine cblock_t
eine Blockanweisung ist, die eine Liste von cinsn_t
Anweisungen ist, wir können diesen Typ zum Beispiel am Anfang einer Funktion oder nach einer bedingten Anweisung finden.
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
};
//...
Im folgenden Beispiel hat der Bedingungsknoten vom Typ cit_if
zwei untergeordnete Knoten: Der linke ist vom Typ cit_block
, der den Zweig "True" darstellt, und der rechte ist die auszuwertende Bedingung, bei der es sich um einen Aufruf einer Funktion handelt, ein drittes untergeordnetes Element fehlt, da die Bedingung keinen "False"-Zweig hat.
Im Folgenden finden Sie ein Diagramm, das den Anweisungsknoten cit_if
Suchen Sie die zugehörige Dekompilierung für das obige CTree:
Die gleiche Logik gilt für Ausdrucksknoten cexpr_t
je nach Knotentyp stehen unterschiedliche Attribute zur Verfügung, z. B. ein Knoten vom Typ cot_asg
über untergeordnete Knoten verfügt, auf die mit den Feldern x
und y
zugegriffen werden kann.
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)
};
};
//...
Schließlich enthält die cfunc_t
-Struktur Informationen über die dekompilierte Funktion, die Funktionsadresse, das Mikrocode-Block-Array und das CTree, auf das mit den Feldern entry_ea
, mba
und body
zugegriffen wird.
struct cfunc_t
{
ea_t entry_ea; ///< function entry address
mba_t *mba; ///< underlying microcode
cinsn_t body; ///< function body, must be a block
//...
Beispiel für CTree-Traversal
Der bereitgestellte Python-Code dient als mini-rekursiver Besucher eines CTree, beachten Sie, dass er nicht alle Knotentypen verarbeitet, im letzten Abschnitt wird beschrieben, wie die in Hex-Rays integrierte Besucherklasse ctree_visitor_t
verwendet wird. Zu Beginn erhalten wir die cfunc
der Funktion mit ida_hexrays.decompile
und greifen über das Feld body
auf ihr CTree zu.
Als nächstes prüfen wir, ob es sich bei dem node(item) um einen Ausdruck oder eine Anweisung handelt. Schließlich können wir den Typ über das Feld op
analysieren und seine untergeordneten Knoten untersuchen.
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()
Unten sehen Sie die Ausgabe des Traversal-Skripts, das auf der start
Funktion einer BLISTER-Probe ausgeführt wird:
Praktisches Beispiel: Annotation der benutzerdefinierten Importtabelle eines Malware-Samples
Nachdem wir nun Einblicke in die Architektur und die Strukturen des generierten CTree erhalten haben, lassen Sie uns in eine praktische Anwendung eintauchen und untersuchen, wie die Annotation einer benutzerdefinierten Importtabelle von Malware automatisiert werden kann.
Hex-Rays bietet eine Utility-Klasse ctree_visitor_t
, die zum Durchlaufen und Ändern des CTree verwendet werden kann, zwei wichtige virtuelle Methoden, die Sie kennen sollten:
visit_insn
: um eine Erklärung zu besuchenvisit_expr
: um einen Ausdruck zu besuchen
In diesem Beispiel wird dieselbe BLISTER-Probe verwendet. nachdem Sie die Funktion gefunden haben, die Windows-API-Adressen per Hash an der Adresse 0x7FF8CC3B0926 (in der RSRC-Datei -Abschnitt), indem wir die Enumeration zur IDB hinzufügen und den Enumerationstyp auf ihren Parameter anwenden, erstellen wir eine Klasse, die von ctree_visitor_t
erbt, da wir an Ausdrücken interessiert sind, werden wir nur visit_expr
überschreiben.
Die Idee ist, einen cot_call
node(1) der Funktion zu finden, die APIs auflöst, indem die obj_ea
Adresse des ersten untergeordneten Elements des Knotens an die Funktion übergeben wird idc.get_name
die den Funktionsnamen zurückgibt.
if expr.op == idaapi.cot_call:
if idc.get_name(expr.x.obj_ea) == self.func_name:
#...
Als nächstes rufen Sie die Enumeration des Hashs ab, indem Sie auf den rechten Parameter des Aufrufs node(2) zugreifen, in unserem Fall auf Parameter 3.
carg_1 = expr.a[HASH_ENUM_INDEX]
api_name = ida_lines.tag_remove(carg_1.cexpr.print1(None)) # Get API name
Der nächste Schritt besteht darin, die Variable zu suchen, der der Adresswert der WinAPI-Funktion zugewiesen wurde. Um dies zu tun, müssen wir zuerst den cot_asg
node(3) lokalisieren, der dem Aufrufknoten übergeordnet ist, indem wir die Methode find_parent_of
unter cfunc.body
der dekompilierten Funktion verwenden.
asg_expr = self.cfunc.body.find_parent_of(expr) # Get node parent
Schließlich können wir auf den ersten untergeordneten node(4) unter dem cot_asg
-Knoten zugreifen, der vom Typ cot_var
ist, und den aktuellen Variablennamen erhalten, die Hex-Rays API -ida_hexrays.rename_lvar
verwendet wird, um die neue Variable mit dem Windows-API-Namen umzubenennen, der aus dem enum-Parameter übernommen wurde.
Dieser Prozess kann einem Analysten letztendlich eine erhebliche Zeitersparnis bringen. Anstatt Zeit mit dem Umbenennen von Variablen zu verbringen, können sie ihre Aufmerksamkeit auf die Kernfunktionalität richten. Ein Verständnis dafür, wie CTrees funktionieren, kann zur Entwicklung effektiverer Plugins beitragen, die den Umgang mit komplexeren Verschleierungen ermöglichen.
Für ein vollständiges Verständnis und den Kontext des Beispiels finden Sie den gesamten Code unten:
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()
Fazit
Zum Abschluss unserer Erkundung des Hex-Rays-Mikrocodes und der CTree-Generierung haben wir praktische Techniken entwickelt, um die Komplexität der Malware-Verschleierung zu bewältigen. Die Möglichkeit, Hex-Rays-Pseudocode zu modifizieren, ermöglicht es uns, Verschleierungen wie Control Flow-Verschleierung zu durchbrechen, toten Code zu entfernen und vieles mehr. Das Hex-Rays C++ SDK erweist sich als wertvolle Ressource und bietet eine gut dokumentierte Anleitung zum späteren Nachschlagen.
Wir hoffen, dass dieser Leitfaden für andere Forscher und alle begeisterten Lernenden hilfreich sein wird, bitte finden Sie alle Skripte in unserem Forschungsrepositorium.