Salim Bitam

Einführung in die Interna der Hex-Rays-Dekompilierung

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.

25 Minuten LesezeitMalware-Analyse
Einführung in die Interna der Hex-Rays-Dekompilierung

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:

  1. Assembler-Code in Mikrocode:
    Es führt eine Konvertierung der Assembleranweisungen, die in einer insn_t Struktur gespeichert sind, in Mikrocode-Anweisungen durch, die durch eine minsn_t Struktur dargestellt werden

  2. 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 sowohl cinsn_t als auch cexpr_t von der citem_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_GENERATEDdie 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_tist, sie enthält allgemeine Informationen wie die Adresse, den Elementtyp und die Beschriftung, während sie auch Konstanten wie is_exprenthä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_tje 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 yzugegriffen 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_tverwendet 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 besuchen
  • visit_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_terbt, 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.

Ressourcen