はじめに
本書では、Hex-Raysマイクロコードを掘り下げ、生成されたCTreeを操作して逆コンパイルされたコードの難読化を解除し、注釈を付けるための手法を探ります。 最後のセクションには、マルウェア分析のためにカスタムインポートテーブルに注釈を付ける方法を示す実際の例が含まれています。
このガイドは、リバースエンジニアやマルウェアアナリストが、IDAの機能逆コンパイル中に使用される内部構造をよりよく理解するのを支援することを目的としています。 IDA PROのpluginsディレクトリにある Hex-Rays SDK には、以下で説明するすべての構造がそこから供給されているので、注意していただくことをお勧めします。
アーキテクチャー
Hex-Raysは、関数の逆アセンブルされたコードから始まる多段階のプロセスを通じて関数を逆コンパイルします。
-
アセンブリ コードをマイクロコードへ変換する:
これは、insn_t
構造体に格納されているアセンブリ命令を、minsn_t
構造体で表されるマイクロコード命令に変換します -
CTree の生成:
最適化されたマイクロコードから、Hex-Rays は Abstract Syntax Tree (AST) を生成します。そのノードはステートメント (cinsn_t
) または式 (cexpr_t
) です。cinsn_t
とcexpr_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_t
とcexpr_t
の両方の基本となる抽象クラスであり、アドレス、アイテムタイプ、ラベルなどの共通情報を保持しながら、オブジェクトのタイプを知るために使用できるcontains_expr
is_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_ea
、 mba
、および 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 は貴重なリソースとして浮上しており、将来の参照用に十分に文書化されたガイダンスを提供します。
このガイドが仲間の研究者や熱心な学習者に役立つことを願っています、私たちの 研究リポジトリですべてのスクリプトを見つけてください。