Salim Bitam

헥사 레이 디컴파일 내부 소개

이 게시글에서는 Hex-Rays 마이크로코드를 살펴보고 생성된 CTree를 조작하여 디컴파일된 코드의 난독화를 해제하고 주석을 다는 기술을 살펴봅니다.

25분 읽기Malware 분석
Hex-Rays 디컴파일 내부 소개

서문

이 게시글에서는 Hex-Rays 마이크로코드를 살펴보고 생성된 CTree를 조작하여 디컴파일된 코드의 난독화를 해제하고 주석을 다는 기술을 살펴봅니다. 마지막 섹션에는 맬웨어 분석을 위해 사용자 지정 가져오기 테이블에 주석을 다는 방법을 보여주는 실용적인 예제가 포함되어 있습니다.

이 가이드는 리버스 엔지니어와 멀웨어 분석가가 IDA의 함수 디컴파일 시 사용되는 내부 구조를 더 잘 이해할 수 있도록 돕기 위한 것입니다. 아래에서 설명하는 모든 구조는 IDA PRO의 플러그인 디렉터리에서 찾을 수 있는 Hex-Rays SDK를 주시하는 것이 좋습니다.

아키텍처

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를 사용하여 추출한 마이크로코드와 함께 어셈블리 및 디컴파일된 코드를 표시합니다.

MBA 필드가 있는 디컴파일된 함수의 cfunc_t 구조를 통해 MBA(마이크로코드 블록 배열)에 액세스할 수 있습니다.

팁: ida_hexrays.decompile 을 사용하여 디컴파일된 함수의 cfunc_t 을 얻습니다.

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 필드를 사용하여 동일한 블록의 다음/이전 마이크로코드 인스트럭션에 액세스할 수 있으며, 옵코드 필드는 모든 마이크로코드 인스트럭션의 열거형(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와 같이 주소, 항목 유형 및 레이블과 같은 일반적인 정보를 보유하며 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_tcinsn_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 유형으로 "True" 브랜치를 나타내고 오른쪽은 평가할 조건으로 함수 호출이며, 조건에 "False" 브랜치가 없기 때문에 세 번째 자식이 누락되어 있습니다.

다음은 문 노드 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_ea, mbabody 필드로 액세스한 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 트래버스 예제

제공된 파이썬 코드는 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()

아래는 블리스터 샘플의 start 함수에서 실행된 트래버스 스크립트의 출력입니다:

실제 사례: 멀웨어 샘플의 사용자 지정 가져오기 테이블에 주석 달기

이제 생성된 CTree의 아키텍처와 구조에 대한 인사이트를 얻었으니 실제 애플리케이션을 살펴보고 멀웨어의 사용자 지정 가져오기 테이블에 주석을 자동화하는 방법을 살펴보겠습니다.

Hex-Rays 는 유틸리티 클래스 ctree_visitor_t 를 제공하는데, 여기서 알아두어야 할 두 가지 중요한 가상 메서드는 다음과 같습니다:

  • visit_insn성명서를 방문하려면
  • visit_expr: 표현식을 방문하려면

이 예제에서는 동일한 BLISTER 샘플이 사용되었으며, 0x7FF8CC3B0926 주소에서 해시로 Windows API 주소를 가져오는 함수를 찾은 후 .rsrc의 섹션)에 열거형을 추가하고 해당 매개변수에 열거형을 적용하여 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 node(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 이름으로 새 변수의 이름을 바꿉니다.

이 프로세스는 궁극적으로 분석가의 시간을 크게 절약할 수 있습니다. 변수의 레이블을 다시 지정하는 데 시간을 소비하는 대신 핵심 기능에 집중할 수 있습니다. 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 C++ SDK는 향후 참조할 수 있도록 잘 문서화된 지침을 제공하는 귀중한 리소스로 부상하고 있습니다.

이 가이드가 동료 연구자와 열성적인 학습자에게 도움이 되길 바라며, 모든 스크립트는 리서치 리포지토리에서 찾아보시기 바랍니다.

리소스