Salim Bitam

Hex-Rays 逆コンパイルの内部の概要

本書では、Hex-Raysマイクロコードを掘り下げ、生成されたCTreeを操作して逆コンパイルされたコードの難読化を解除し、注釈を付けるための手法を探ります。

25分で読めますマルウェア分析
Hex-Rays 逆コンパイルの内部の概要

はじめに

本書では、Hex-Raysマイクロコードを掘り下げ、生成されたCTreeを操作して逆コンパイルされたコードの難読化を解除し、注釈を付けるための手法を探ります。 最後のセクションには、マルウェア分析のためにカスタムインポートテーブルに注釈を付ける方法を示す実際の例が含まれています。

このガイドは、リバースエンジニアやマルウェアアナリストが、IDAの機能逆コンパイル中に使用される内部構造をよりよく理解するのを支援することを目的としています。 IDA PROのpluginsディレクトリにある Hex-Rays SDK には、以下で説明するすべての構造がそこから供給されているので、注意していただくことをお勧めします。

アーキテクチャー

Hex-Raysは、関数の逆アセンブルされたコードから始まる多段階のプロセスを通じて関数を逆コンパイルします。

  1. アセンブリ コードをマイクロコードへ変換する:
    これは、insn_t構造体に格納されているアセンブリ命令をminsn_t構造体で表されるマイクロコード命令に変換します

  2. CTree の生成:
    最適化されたマイクロコードから、Hex-Rays は Abstract Syntax Tree (AST) を生成します。そのノードはステートメント (cinsn_t) または式 (cexpr_t) です。cinsn_tcexpr_t の両方が citem_t 構造体から継承されることに注意してください

マイクロ

マイクロコードは、Hex-Raysが使用する中間言語(IL)で、バイナリのアセンブリコードを持ち上げることによって生成されます。 これには複数の利点がありますが、その 1 つはプロセッサに依存しないことです。

次のスクリーンショットは、アセンブリと逆コンパイルされたコードと、マイクロコードの視覚化を容易にするツールである Lucid を使用して抽出されたマイクロコードを示しています。

MBA(マイクロコードブロック配列)には、MBAフィールドを持つ逆コンパイル関数の cfunc_t 構造体を通じてアクセスできます。

ヒント: 逆コンパイルされた関数の cfunc_t は、 ida_hexrays.decompileで得られます。

mba_tはマイクロブロックの配列ですmblock_t、最初のブロックは関数のエントリポイントを表し、最後のブロックは終了を表します。マイクロブロック(mblock_t)は二重リンクリストで構成されており、それぞれ nextb/prevb フィールドを使用して次/前のブロックにアクセスできます。 各mblock_tには、ブロックの最初の命令のフィールドheadとブロックの最後の命令のtailフィールドからアクセスされるマイクロコード命令minsn_tの二重リンクリストが含まれています。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 フィールドは、すべての microinstruction オペコードの列挙 (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を動的にロードするマルウェアのカスタムインポートテーブルに注釈を付ける方法を示す実際の例に進みます。

理解を深めるために、IDAのCTreeノードをグラフとして表示できる次のプラグイン(hrdevhelper)を活用します。

citem_tは、cinsn_tcexpr_tの両方の基本となる抽象クラスであり、アドレス、アイテムタイプ、ラベルなどの共通情報を保持しながら、オブジェクトのタイプを知るために使用できるcontains_expris_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 の条件ノードに 2 つの子ノードがあります: 左側のノードは cit_block 型で "True" 分岐を表し、右側は評価する条件 (関数の呼び出し) で、条件に "False" 分岐がないため 3 番目の子が欠落しています。

以下は、ステートメントノード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 構造体は、逆コンパイルされた関数、関数アドレス、マイクロコードブロック配列、および entry_eamba 、および body フィールドでアクセスされる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にアクセスします。

次に、ノード(item)が式か文かを確認します。 最後に、 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 の走査と変更に使用できるユーティリティ クラス ctree_visitor_t を提供し、次の 2 つの重要な仮想メソッドを知っておく必要があります。

  • visit_insn: ステートメントを訪問する
  • visit_expr: エクスプレッションを訪問する

この例では、同じ BLISTER サンプルを使用します。アドレス 0x7FF8CC3B0926 (.rsrc セクション)、列挙型を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:
		#...

次に、呼び出し node(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を使用して、新しい変数の名前をenumパラメータから取得したWindows API名に変更します。

このプロセスにより、最終的にアナリストの時間を大幅に節約できます。 変数のラベル付けの変更に時間を費やす代わりに、コア機能に注意を向けることができます。 CTrees がどのように機能するかを理解することで、より効果的なプラグインの開発に貢献し、より複雑な難読化の処理が可能になります。

例の完全な理解とコンテキストについては、以下のコード全体を見つけてください。

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 は貴重なリソースとして浮上しており、将来の参照用に十分に文書化されたガイダンスを提供します。

このガイドが仲間の研究者や熱心な学習者に役立つことを願っています、私たちの 研究リポジトリですべてのスクリプトを見つけてください。

各種資料

この記事を共有する