简介
本文深入探讨了 Hex-Rays 微代码,并探索了操纵生成的 CTree 以进行反混淆和注释反汇编代码的技术。最后一部分包含一个实际示例,演示如何为恶意软件分析注释自定义导入表。
本指南旨在帮助逆向工程师和恶意软件分析师更好地理解 IDA 函数反汇编过程中使用的内部结构。我们建议关注可以在 IDA PRO 插件目录中找到的 Hex-Rays SDK,下面讨论的所有结构都来源于此。
架构
Hex-Rays 通过一个多阶段过程反汇编函数,从函数的反汇编代码开始。
-
CTree 生成
从优化的微代码中,Hex-Rays 生成抽象语法树 (AST),其节点要么是语句 (cinsn_t
),要么是表达式 (cexpr_t
);请注意,cinsn_t
和cexpr_t
都继承自citem_t
结构。
微代码
微代码是由 Hex-Rays 使用的中间语言 (IL),它是通过提升二进制文件的汇编代码生成的。这具有多个优点,其中之一是它与处理器无关。
以下屏幕截图显示了汇编和反汇编代码,以及使用 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_t
和 cexpr_t
的基类,它包含诸如地址、项目类型和标签等通用信息,同时也包含诸如 is_expr
、contains_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
的节点具有可通过 x
和 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)
};
};
//...
最后,cfunc_t
结构包含与反编译函数相关的信息,包括函数地址、微代码块数组和CTree,分别通过 entry_ea
、mba
和 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
//...
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样本;在定位通过哈希获取Windows API地址的函数(地址为0x7FF8CC3B0926(在.rsrc节中))后,向IDB添加枚举并将枚举类型应用于其参数,我们创建一个从 ctree_visitor_t
继承的类,因为我们对表达式感兴趣,所以我们只覆盖 visit_expr
。
其思想是定位函数的 cot_call
节点(1),该函数通过将节点第一个子节点的 obj_ea
地址传递给函数 idc.get_name
来解析API,该函数将返回函数名称。
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函数地址值的变量。为此,我们首先需要定位 cot_asg
节点(3),它是调用节点的父节点,方法是使用反编译函数的 cfunc.body
下的 find_parent_of
方法。
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 成为宝贵的资源,为将来参考提供了完善的文档指导。
我们希望本指南对各位研究人员和任何热心学习者有所帮助,请在我们的 研究库 中找到所有脚本。