Salim Bitam

Hex-Rays 反编译内部原理简介

在本文中,我们将深入研究 Hex-Rays 微代码,并探索操纵生成的 CTree 以解混淆和注释反编译代码的技术。

阅读时长 25 分钟恶意软件分析
Introduction to Hex-Rays decompilation internals

简介

在本文中,我们将深入研究 Hex-Rays 微代码,并探索操纵生成的 CTree 以解混淆和注释反编译代码的技术。最后一部分包含一个实际示例,演示如何为恶意软件分析注释自定义导入表。

本指南旨在帮助逆向工程师和恶意软件分析师更好地理解 IDA 函数反编译过程中使用的内部结构。我们建议关注 Hex-Rays SDK,它可以在 IDA PRO 的插件目录中找到,下面讨论的所有结构都来源于此。

架构

Hex-Rays 通过一个多阶段过程反编译函数,从函数的反汇编代码开始

  1. 汇编代码到微代码
    它将存储在 insn_t 结构中的汇编指令转换为由 minsn_t 结构表示的微代码指令

  2. CTree 生成
    从优化的微代码中,Hex-Rays 生成抽象语法树 (AST),其节点是语句 (cinsn_t) 或表达式 (cexpr_t);请注意,cinsn_tcexpr_t 都继承自 citem_t 结构

微代码

微代码是 Hex-Rays 使用的一种中间语言 (IL),通过提升二进制文件的汇编代码生成。这具有多个优点,其中之一是它与处理器无关。

下面的屏幕截图显示了汇编代码和反编译代码,以及使用 Lucid 提取的微代码,Lucid 是一种有助于微代码可视化的工具。

我们可以通过反编译函数的 cfunc_t 结构的 MBA 字段访问 MBA(微代码块数组)。

提示:我们使用 ida_hexrays.decompile 获取反编译函数的 cfunc_t

mba_t 是微块 mblock_t 的数组,第一个块表示函数的入口点,最后一个块表示结束。微块 (mblock_t) 以双向链表结构组织,我们可以分别使用 nextb/prevb 字段访问下一个/上一个块。每个 mblock_t 都包含一个微代码指令 minsn_t 的双向链表,通过字段 head 访问块的第一个指令,通过字段 tail 访问块的最后一个指令。mblock_t 结构如下图所示。

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;

微代码指令 minsn_t 是一个双向链表,每个微代码指令包含 3 个操作数:左、右和目标。我们可以使用 next/prev 字段访问同一块的下一个/上一个微代码指令;opcode 字段是所有微指令操作码的枚举 (mcode_t),例如,m_mov 枚举表示 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
//...
};

每个操作数的类型为 mop_t,根据类型(使用 t 字段访问)它可以保存寄存器、立即值,甚至是嵌套的微代码指令。例如,以下是一个具有多个嵌套指令的函数的微代码

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)
  #...

微代码生成会经历不同的成熟度级别,也称为优化级别。初始级别 MMAT_GENERATED 涉及将汇编代码直接转换为微代码。生成 CTree 之前的最终优化级别是 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
};

微代码遍历示例

以下 Python 代码用作如何遍历和打印函数的微代码指令的示例,它遍历在第一个成熟度级别 (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()

脚本的输出如下所示:左侧是在控制台中打印的微代码,右侧是 IDA 的汇编代码

CTree

在本节中,我们将深入探讨 Hex-Rays CTree 结构的核心要素,然后继续进行一个实际示例,演示如何注释恶意软件的自定义导入表,该恶意软件会动态加载 API。

为了更好地理解,我们将利用以下插件 (hrdevhelper),该插件允许我们在 IDA 中以图形方式查看 CTree 节点。

citem_t 是一个抽象类,它是 cinsn_tcexpr_t 的基类。它包含诸如地址、项类型和标签等公共信息,同时还具有诸如 is_exprcontains_expr 这样的常量,可以用来了解对象的类型。

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()
//...

通过 op 字段访问的项类型指示节点的类型。表达式节点以 cot_ 为前缀,语句节点以 cit_ 为前缀。例如,cot_asg 表示节点是赋值表达式,而 cit_if 表示节点是条件(if)语句。

根据语句节点的类型,cinsn_t 可以具有不同的属性。例如,如果项类型是 cit_if,我们可以通过 cif 字段访问条件节点的详细信息。如下面的代码片段所示,cinsn_t 是使用联合体实现的。请注意,cblock_t 是一个块语句,它是 cinsn_t 语句的列表。例如,我们可以在函数的开头或条件语句之后找到这种类型。

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
  };
//...

在下面的示例中,类型为 cit_if 的条件节点有两个子节点:左边的子节点是 cit_block 类型,表示“真”分支;右边的子节点是要评估的条件,是对函数的调用。由于条件没有“假”分支,因此缺少第三个子节点。

以下是展示语句节点 cit_if 的图表

找到上述 CTree 的相关反编译结果

相同的逻辑适用于表达式节点 cexpr_t。根据节点类型,可以使用不同的属性。例如,类型为 cot_asg 的节点具有可以通过字段 xy 访问的子节点。

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)
      };
    };
//...

最后,cfunc_t 结构包含与反编译函数相关的信息,包括函数地址、微代码块数组以及可以通过 entry_eambabody 字段访问的 CTree。

struct cfunc_t
{
  ea_t entry_ea;             ///< function entry address
  mba_t *mba;                   ///< underlying microcode
  cinsn_t body;              ///< function body, must be a block
//...

CTree 遍历示例

提供的 Python 代码充当 CTree 的微型递归访问器。请注意,它不处理所有节点类型。最后一节将介绍如何使用 Hex-Rays 内置的访问器类 ctree_visitor_t。首先,我们使用 ida_hexrays.decompile 获取函数的 cfunc,并通过 body 字段访问其 CTree。

接下来,我们检查节点(项)是表达式还是语句。最后,我们可以通过 op 字段解析类型,并探索其子节点。

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()

下面显示的是在 BLISTER 样本start 函数上执行遍历脚本的输出。

实际示例:注释恶意软件样本的自定义导入表

现在我们已经深入了解了生成的 CTree 的架构和结构,让我们深入研究一个实际应用,并探索如何自动化注释恶意软件的自定义导入表。

Hex-Rays 提供了一个实用程序类 ctree_visitor_t,可用于遍历和修改 CTree。两个重要的虚拟方法是:

  • visit_insn:用于访问语句
  • visit_expr:用于访问表达式

在本例中,使用了相同的 BLISTER 样本。在地址 0x7FF8CC3B0926(在 .rsrc 部分)找到通过哈希获取 Windows API 地址的函数后,将枚举添加到 IDB 并将其枚举类型应用于其参数,我们创建一个继承自 ctree_visitor_t 的类。由于我们对表达式感兴趣,我们将只重写 visit_expr

其思路是定位通过传递 obj_ea 地址给函数 idc.get_name 来解析 API 的函数的 cot_call 节点 (1),该函数将返回函数名称。

   if expr.op == idaapi.cot_call:
            if idc.get_name(expr.x.obj_ea) == self.func_name:
		#...

接下来,通过访问调用节点的右侧参数 (2)(在本例中是参数 3)来检索哈希的枚举。

    carg_1 = expr.a[HASH_ENUM_INDEX]
    api_name = ida_lines.tag_remove(carg_1.cexpr.print1(None))  # Get API name

下一步是定位已被分配 WinAPI 函数地址值的变量。为此,我们首先需要使用反编译函数的 cfunc.body 下的 find_parent_of 方法来定位调用节点的父节点 cot_asg 节点 (3)。

    asg_expr = self.cfunc.body.find_parent_of(expr)  # Get node parent

最后,我们可以访问 cot_asg 节点下的第一个子节点 (4),该子节点是 cot_var 类型,并获取当前变量名称。Hex-Rays API ida_hexrays.rename_lvar 用于使用从枚举参数获取的 Windows API 名称重命名新变量。

这个过程最终可以为分析师节省大量时间。他们可以将注意力集中在核心功能上,而不是花时间重新标记变量。了解 CTree 的工作原理有助于开发更有效的插件,从而能够处理更复杂的混淆。

为了全面理解示例的上下文,请在下面查找完整代码

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()

结论

在结束我们对 Hex-Rays 微代码和 CTree 生成的探索之际,我们已经掌握了应对恶意软件混淆复杂性的实用技术。修改 Hex-Rays 伪代码的能力使我们能够消除诸如控制流混淆之类的混淆,删除无用代码等等。Hex-Rays C++ SDK 作为一个宝贵的资源出现,为将来的参考提供了充分的文档指导。

我们希望本指南对其他研究人员和任何热衷于学习的人有所帮助。请在我们的 研究存储库中找到所有脚本。

资源